commit e26b7960fb08d31059a93b60a0c76df4bffa7969 Author: Lxy Date: Sun Apr 12 11:16:11 2026 +0800 feat: 初始化工程,从github更新代码(4月12日最后更新) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..56e3a53 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +# 忽略 Python 缓存 +__pycache__/ +*.py[cod] +*$py.class +*.so + +# 忽略虚拟环境 +venv/ +.venv/ +env/ +.env.local + +# 忽略 IDE +.idea/ +.vscode/ +*.swp +*.swo + +# 忽略数据文件(可选,如果想持久化就注释掉) +# data/ +# logs/ +# reports/ + +# 忽略测试文件 +test_*.py +*_test.py + +# 忽略文档 +*.md +!README.md diff --git a/.git---/FETCH_HEAD b/.git---/FETCH_HEAD new file mode 100644 index 0000000..0427ee2 --- /dev/null +++ b/.git---/FETCH_HEAD @@ -0,0 +1,169 @@ +3dceb1eca8487e5763b5995f78dd9d723213423d branch 'main' of https://github.com/ZhuLinsen/daily_stock_analysis +613bf2bcfdcdf63fe22c537c8831ee9fc02d44e4 not-for-merge branch 'autocode/issue-1027-feature-stocks-index-json-90-sto' of https://github.com/ZhuLinsen/daily_stock_analysis +1840f112fb724e7ba7e8dbb3d35808b64a12b8d2 not-for-merge branch 'autocode/issue-1029-feature-report-language' of https://github.com/ZhuLinsen/daily_stock_analysis +9990f21e8fa42d3d0c997e1bbdcc32fbdeabd5f0 not-for-merge branch 'autocode/issue-594-feature' of https://github.com/ZhuLinsen/daily_stock_analysis +3f1cfa1902caf7233526b2b3a24e1a5f0462c51f not-for-merge branch 'autocode/issue-637-feature' of https://github.com/ZhuLinsen/daily_stock_analysis +01f72edee9fe0cbf0eb7dcefbea7ad5983ee54a2 not-for-merge branch 'autocode/issue-751-feature-windows-mac' of https://github.com/ZhuLinsen/daily_stock_analysis +8caf7445e90fe65d11b8418a808b1e668da7ad81 not-for-merge branch 'autocode/issue-765-bug-docker-webui' of https://github.com/ZhuLinsen/daily_stock_analysis +027272a1a0e1b6007b5f767128dacfac0d6717cd not-for-merge branch 'autocode/issue-773-bug' of https://github.com/ZhuLinsen/daily_stock_analysis +e08619621b3b6a82ff6e92ed8855c40d5943a8c0 not-for-merge branch 'autocode/issue-792-task' of https://github.com/ZhuLinsen/daily_stock_analysis +3bdad39fc336edb678603bc4b0e7ddb33c3cae98 not-for-merge branch 'autocode/issue-796-bug-docker-web' of https://github.com/ZhuLinsen/daily_stock_analysis +e7757b0db95a74f50d2d0b8b3e209a4e7e102b49 not-for-merge branch 'autocode/issue-812-feature-ai' of https://github.com/ZhuLinsen/daily_stock_analysis +84b635a6e04940f14f2421fefff42ba312a03307 not-for-merge branch 'autocode/issue-819-bug' of https://github.com/ZhuLinsen/daily_stock_analysis +7fbb389ec11c8eeb984ec4e9e73ce0582ce4360b not-for-merge branch 'autocode/issue-821-feature' of https://github.com/ZhuLinsen/daily_stock_analysis +d3aa23f0975b5472aa6dd934acc16f7569d19aa1 not-for-merge branch 'autocode/issue-825-task' of https://github.com/ZhuLinsen/daily_stock_analysis +2b5808f4683795ebffc02a95edab9d5195efd849 not-for-merge branch 'autocode/issue-826-feature-release-win-minimax' of https://github.com/ZhuLinsen/daily_stock_analysis +8449670a5fe194e32cba254d62680da01496043e not-for-merge branch 'autocode/issue-827-feature' of https://github.com/ZhuLinsen/daily_stock_analysis +38596a2bbe2e2a32f071aaf850670fca03096135 not-for-merge branch 'autocode/issue-830-feature-v1-models' of https://github.com/ZhuLinsen/daily_stock_analysis +7dd3a226cb6329e0b359716c26c37418b01fc959 not-for-merge branch 'autocode/issue-832-bug' of https://github.com/ZhuLinsen/daily_stock_analysis +e60cd15a2bcb63478f15c95184bc5ef9c2eb17fe not-for-merge branch 'autocode/issue-839-bug' of https://github.com/ZhuLinsen/daily_stock_analysis +ea946fce0754734e4eb6043d064ec47f6bae38b7 not-for-merge branch 'autocode/issue-840-security-litellm-pypi-1-82-7-1-8' of https://github.com/ZhuLinsen/daily_stock_analysis +2c47d6ebd210d6c0f5b023b30a4a2c65d5e94505 not-for-merge branch 'autocode/issue-848-bug-the-job-has-exceeded-the-max' of https://github.com/ZhuLinsen/daily_stock_analysis +f49f2884d13042a3194d209eedd54c1a780eccf3 not-for-merge branch 'autocode/issue-854-ollama' of https://github.com/ZhuLinsen/daily_stock_analysis +cddb351e375dcde570341e96009cbb70e0262e51 not-for-merge branch 'autocode/issue-860-bug-stock-group-email-group-gith' of https://github.com/ZhuLinsen/daily_stock_analysis +7c1efc6e7652ce641cf0275906582f714ba5996d not-for-merge branch 'autocode/issue-863-feature-change-the-max-tokens-fr' of https://github.com/ZhuLinsen/daily_stock_analysis +9ae8b04a91be289bbe1906182d16894b4faca772 not-for-merge branch 'autocode/issue-865-feature-llm-stream' of https://github.com/ZhuLinsen/daily_stock_analysis +5e94755c88b879eff71f893a71368497d1572a72 not-for-merge branch 'autocode/issue-872-feature-github-action-ai' of https://github.com/ZhuLinsen/daily_stock_analysis +6b7ab16a111890734935973f9e92f443460908f7 not-for-merge branch 'autocode/issue-875-feature-litellm' of https://github.com/ZhuLinsen/daily_stock_analysis +02c2ee45efc9f320f43b4942074b866fc2b54c9e not-for-merge branch 'autocode/issue-876-bug' of https://github.com/ZhuLinsen/daily_stock_analysis +16c9bab53ef82bc869090e47cf94ac3307662ba8 not-for-merge branch 'autocode/issue-877-bug-info-llm-prompt-response' of https://github.com/ZhuLinsen/daily_stock_analysis +778168f9de58d66721e013530f932197a1fe4af7 not-for-merge branch 'autocode/issue-878-bug-sqlite' of https://github.com/ZhuLinsen/daily_stock_analysis +7175dd29259de4084b8f477e4301a500911147a8 not-for-merge branch 'autocode/issue-880-bug' of https://github.com/ZhuLinsen/daily_stock_analysis +cc745ac641fe0e43b3e272669ca53b613b1c51b4 not-for-merge branch 'autocode/issue-881-bug' of https://github.com/ZhuLinsen/daily_stock_analysis +183af0af2520385e44f03bd05271b698794ba6b7 not-for-merge branch 'autocode/issue-882-feature-serpapi' of https://github.com/ZhuLinsen/daily_stock_analysis +4b23a4b9986d53fc8531fc4c7e12436b684a5c94 not-for-merge branch 'autocode/issue-883-docs-readme' of https://github.com/ZhuLinsen/daily_stock_analysis +44ca7f54afbc2d8eb2031fd0002ca8eda64f4b40 not-for-merge branch 'autocode/issue-896-bug-discord-webhook' of https://github.com/ZhuLinsen/daily_stock_analysis +3e31b26e72e4f599892597bb148a28bed5ae09cd not-for-merge branch 'autocode/issue-901-task' of https://github.com/ZhuLinsen/daily_stock_analysis +42e0e0be50e956372dedf6b212ce11cd01701f73 not-for-merge branch 'autocode/issue-902-feature-clawbot' of https://github.com/ZhuLinsen/daily_stock_analysis +075cbf98e25b3ae38b779c6be9208005efb55964 not-for-merge branch 'autocode/issue-940-bug-stock-name' of https://github.com/ZhuLinsen/daily_stock_analysis +3bdf9903b682bbcb5163c36c5880f5979c035038 not-for-merge branch 'autocode/issue-942-bug' of https://github.com/ZhuLinsen/daily_stock_analysis +8e3e5c72407e0fbf0610948366de6aefef0d74ce not-for-merge branch 'autocode/issue-944-feature-webui-ui' of https://github.com/ZhuLinsen/daily_stock_analysis +19cb475350d1176b69390c3a8baef0571be39969 not-for-merge branch 'autocode/issue-946-bug' of https://github.com/ZhuLinsen/daily_stock_analysis +f488e76cc1128218882df33fd28b11a396ef4194 not-for-merge branch 'autocode/issue-967-bug-sse-cancellederror-re-raise' of https://github.com/ZhuLinsen/daily_stock_analysis +7b9c03c6401b3332e7e202e312f5c72630c2a7e0 not-for-merge branch 'autocode/issue-968-bug-bot-ask' of https://github.com/ZhuLinsen/daily_stock_analysis +f6a88b85826497ef1888b939974965851b627315 not-for-merge branch 'autocode/issue-969-bug-agent-sse' of https://github.com/ZhuLinsen/daily_stock_analysis +6cdbd682eb4c088e20cd1c2795fb766ff9087da0 not-for-merge branch 'autocode/issue-970-bug-ask' of https://github.com/ZhuLinsen/daily_stock_analysis +fd47fc20b6af24d9eaa2acfa2703d2d2d81a0ee0 not-for-merge branch 'autocode/issue-976-feature-bug' of https://github.com/ZhuLinsen/daily_stock_analysis +660ff09fde3d88b0e0a4cded20b2f2a9c8ceaecc not-for-merge branch 'autocode/issue-983-bug' of https://github.com/ZhuLinsen/daily_stock_analysis +ffc8e725ee17528da8d3c686bd40c1500025ec8e not-for-merge branch 'autocode/issue-996-bug' of https://github.com/ZhuLinsen/daily_stock_analysis +3c997f4811f2c3d850dc8977c5cb036c74cae00e not-for-merge branch 'autocode/issue-998-bug-a' of https://github.com/ZhuLinsen/daily_stock_analysis +4d7aa88c7d919edb337777369d08592981eda108 not-for-merge branch 'bugfix/package-import-error' of https://github.com/ZhuLinsen/daily_stock_analysis +046a7166d7303ed17522418133c3fad778b40e91 not-for-merge branch 'chore/ai-governance-hardening' of https://github.com/ZhuLinsen/daily_stock_analysis +fcf333798904385de4ad956663c63fafecb24a57 not-for-merge branch 'chore/changelog-flat-unreleased-format' of https://github.com/ZhuLinsen/daily_stock_analysis +e0d8ed566799dec2211a6bd52cdb8503cb897641 not-for-merge branch 'copilot/docs-simplify-readme-structure' of https://github.com/ZhuLinsen/daily_stock_analysis +26f2e2e162dc8c2aca605dfb17cb0b95792e0192 not-for-merge branch 'copilot/sub-pr-641' of https://github.com/ZhuLinsen/daily_stock_analysis +8ee34dbb1c4241cf0e705e17b9922eabd3257d86 not-for-merge branch 'copilot/sub-pr-648' of https://github.com/ZhuLinsen/daily_stock_analysis +70227db11512d524138634e5d11734265ed0970f not-for-merge branch 'copilot/validate-branch-content-and-issues' of https://github.com/ZhuLinsen/daily_stock_analysis +ae4aef3efa852fb40f258592556265c0774cb0f3 not-for-merge branch 'docs/governance-doc-sync-rules' of https://github.com/ZhuLinsen/daily_stock_analysis +6c914ff91e434c7a823331ed08ed3e8d7b3b94a3 not-for-merge branch 'docs/i18n-improvements' of https://github.com/ZhuLinsen/daily_stock_analysis +144e015a8073ade434709ae5288825b1c5e2b344 not-for-merge branch 'docs/trendshift-readme-badge' of https://github.com/ZhuLinsen/daily_stock_analysis +dba9f3a3e4b8b419e7037726dfb1b1e8a46b3351 not-for-merge branch 'feat/779-skill-alignment' of https://github.com/ZhuLinsen/daily_stock_analysis +b91b01d5ec4dae06eeec1fca1201fe5f60147152 not-for-merge branch 'feat/ai-review-ci-context' of https://github.com/ZhuLinsen/daily_stock_analysis +9b9e73f0c09587385f4423043d7bb95dc01d6322 not-for-merge branch 'feat/bot-commands-dispatch' of https://github.com/ZhuLinsen/daily_stock_analysis +2b152174b95f15c176485ce095b7f43388845333 not-for-merge branch 'feat/deep-research-events' of https://github.com/ZhuLinsen/daily_stock_analysis +2c3d44ca9fc4a193358787d62b1d3b1e600bf45f not-for-merge branch 'feat/issue-669-related-boards' of https://github.com/ZhuLinsen/daily_stock_analysis +dd4c7184d1c8c13417bcb75eb75cb7cdd2201b42 not-for-merge branch 'feat/issue-754-desktop-env-backup' of https://github.com/ZhuLinsen/daily_stock_analysis +6937a1521d782410ea50c16a982ffd58709aae2a not-for-merge branch 'feat/llm-config-usability' of https://github.com/ZhuLinsen/daily_stock_analysis +28126db5bd7bcba9897063831f72dce1ccef6cf3 not-for-merge branch 'feat/multi-agent-core' of https://github.com/ZhuLinsen/daily_stock_analysis +b08e14012b5322701354155a37a0cb676e5713b3 not-for-merge branch 'feat/multi-agents-arch' of https://github.com/ZhuLinsen/daily_stock_analysis +9cd1d2f1ea96fb0dea6f2bbd0f1b6c5be16a1add not-for-merge branch 'feat/report-language-758' of https://github.com/ZhuLinsen/daily_stock_analysis +50c4171736680da3c44cba9a5009e2ecb31bb7dc not-for-merge branch 'fix-analysis-api-batch-contract' of https://github.com/ZhuLinsen/daily_stock_analysis +a9af3957f3aab155d67cd0645336160cabcaabf0 not-for-merge branch 'fix/agent-max-steps-ceiling' of https://github.com/ZhuLinsen/daily_stock_analysis +e22278eb904543c5db177084488fcabb627cc101 not-for-merge branch 'fix/agent-min-budget-guard' of https://github.com/ZhuLinsen/daily_stock_analysis +0f49c461f2d95af02b780eebd48d5871a278d47d not-for-merge branch 'fix/backend-ci-single-install' of https://github.com/ZhuLinsen/daily_stock_analysis +b30873901ed74126648eabebb919b8a6eb8effab not-for-merge branch 'fix/daily-analysis-timeout-increase' of https://github.com/ZhuLinsen/daily_stock_analysis +283f01306255ef2276fe90dc5391faaddbb13ec1 not-for-merge branch 'fix/email-sender-name-encoding-708' of https://github.com/ZhuLinsen/daily_stock_analysis +ffbe1e178a792597222ac6084bde0a270a633806 not-for-merge branch 'fix/github-actions-node24-upgrade' of https://github.com/ZhuLinsen/daily_stock_analysis +362a7bb9b1d6c894de2cfdd507d499d5605b4231 not-for-merge branch 'fix/hk-stock-code-629-691' of https://github.com/ZhuLinsen/daily_stock_analysis +b318bcdacc6f04edb7c0a3725d4909e12eb7abcc not-for-merge branch 'fix/issue-726-schedule-immediate-compat' of https://github.com/ZhuLinsen/daily_stock_analysis +d411b96c0f9c8457f6471b223cd9519efd0c781e not-for-merge branch 'fix/issue-749-copy-button' of https://github.com/ZhuLinsen/daily_stock_analysis +666665a78b681ba35241b3d607860624b9eb1088 not-for-merge branch 'fix/issue-772-portfolio-fx-refresh-disabled' of https://github.com/ZhuLinsen/daily_stock_analysis +e9a050a2ef1f359061d1c3e7466183f89dd87e87 not-for-merge branch 'fix/issue-782-tavily-news-filter' of https://github.com/ZhuLinsen/daily_stock_analysis +a6fc1c53fb5d395863906550c2989fb481e5c8ca not-for-merge branch 'fix/llm-rate-limit-detection' of https://github.com/ZhuLinsen/daily_stock_analysis +e4009e16f861ceac8a854e1d20611e2cce4cc93e not-for-merge branch 'fix/multi-agent-max-steps-and-graceful-degradation' of https://github.com/ZhuLinsen/daily_stock_analysis +229d6a3aa3dc4115f781aefdd0f0735682301aee not-for-merge branch 'fix/p0-stability-hardening' of https://github.com/ZhuLinsen/daily_stock_analysis +68ff8eaaba44d6da3359c6f38df3ef7573c0844f not-for-merge branch 'fix/pipeline-optional-service-resilience' of https://github.com/ZhuLinsen/daily_stock_analysis +24e011636d434f2fb610bf74555c5bb99afb8d7d not-for-merge branch 'fix/post-merge-auth-tushare-followups' of https://github.com/ZhuLinsen/daily_stock_analysis +8db6e3870abea74362ef89d69a2adb5093e8639e not-for-merge branch 'fix/realtime-indicator-date-tz-test' of https://github.com/ZhuLinsen/daily_stock_analysis +8711ab653bd7192f23cb566f27fe1231df686992 not-for-merge branch 'fix/recent-bug-triage' of https://github.com/ZhuLinsen/daily_stock_analysis +0372479645b6d72d9088d4ec12f07c102bd978f4 not-for-merge branch 'fix/recover-pr-648-649' of https://github.com/ZhuLinsen/daily_stock_analysis +af0b67cc04006d176d6da1d0a91d38c2f4027347 not-for-merge branch 'fix/report-details-copy-feedback-pr' of https://github.com/ZhuLinsen/daily_stock_analysis +92a5f68fb1eaf106adf606b7253f4b1f6a219905 not-for-merge branch 'fix/report-language-workflow-and-status-price' of https://github.com/ZhuLinsen/daily_stock_analysis +08bb61b9d11366c435bc22388614bc8f98f0fee1 not-for-merge branch 'fix/restore-pypi-litellm' of https://github.com/ZhuLinsen/daily_stock_analysis +77e37214a662f1e5061bfbee3e3925f5ecc37a51 not-for-merge branch 'fix/runtime-robustness' of https://github.com/ZhuLinsen/daily_stock_analysis +3c378308b48b6f9b19a4a590805d396cf668c427 not-for-merge branch 'fix/skill-compat-followups' of https://github.com/ZhuLinsen/daily_stock_analysis +07c8e19135f805ccd6f51913a077a65642f2073e not-for-merge branch 'fix/trading-philosophy-injection-complete' of https://github.com/ZhuLinsen/daily_stock_analysis +528d39411253b6deb18d5da49d7a6d2ae607263f not-for-merge branch 'fix/tushare-http-client-init' of https://github.com/ZhuLinsen/daily_stock_analysis +8db149f1b0da2258ec15ba8e5a101f6785c297ff not-for-merge branch 'pr-919' of https://github.com/ZhuLinsen/daily_stock_analysis +2692b20c2d5a372444c436652ea66998262cb1f6 not-for-merge tag 'v3.1.10' of https://github.com/ZhuLinsen/daily_stock_analysis +b19a4d0df9c040907965ab177df5f775ecdf9aa2 not-for-merge tag 'v3.1.11' of https://github.com/ZhuLinsen/daily_stock_analysis +542c8e4314fe2955a4050f56f2d2c93df8e15690 not-for-merge tag 'v3.1.12' of https://github.com/ZhuLinsen/daily_stock_analysis +5414a6de80442388b1c7b0eb0ae3a8aba3d7dfa1 not-for-merge tag 'v3.1.13' of https://github.com/ZhuLinsen/daily_stock_analysis +84a3da99533bdf9f994f81fd4bb2f6dfb4d43091 not-for-merge tag 'v3.1.8' of https://github.com/ZhuLinsen/daily_stock_analysis +6a7c08d52ed056b75f11e8dd00a341fa88df478a not-for-merge tag 'v3.1.9' of https://github.com/ZhuLinsen/daily_stock_analysis +edd51c858d06d68d964f1c9c5f651b4a827b142d not-for-merge tag 'v3.10.0' of https://github.com/ZhuLinsen/daily_stock_analysis +4914a95cab165b19e183ff37dbbecdb42ce8f9eb not-for-merge tag 'v3.10.1' of https://github.com/ZhuLinsen/daily_stock_analysis +863c268053d3a65b65c9c3a8d7f444d026e584c3 not-for-merge tag 'v3.11.0' of https://github.com/ZhuLinsen/daily_stock_analysis +d1bb085c7cafef762b38b78e67984a7eaf1a237b not-for-merge tag 'v3.12.0' of https://github.com/ZhuLinsen/daily_stock_analysis +498770616f43ef3a554e249b87be7372928f1aa7 not-for-merge tag 'v3.2.0' of https://github.com/ZhuLinsen/daily_stock_analysis +b8ca520f195b98421b4b86e44118d2a939fb6fbe not-for-merge tag 'v3.2.1' of https://github.com/ZhuLinsen/daily_stock_analysis +e780f840a33c1adc78e92a8392789029ac37fb87 not-for-merge tag 'v3.2.10' of https://github.com/ZhuLinsen/daily_stock_analysis +c9b01fead240b7ccbecdd0448350ac32feb45622 not-for-merge tag 'v3.2.11' of https://github.com/ZhuLinsen/daily_stock_analysis +7cf19251798a01238f8bc681f1cf24ccb83f0131 not-for-merge tag 'v3.2.2' of https://github.com/ZhuLinsen/daily_stock_analysis +648bf5b6384a2cf3bb6bf53316979b9d1d0b03c5 not-for-merge tag 'v3.2.3' of https://github.com/ZhuLinsen/daily_stock_analysis +9955b8ab520b8154111dd251aa65d9ba5c0b8d4a not-for-merge tag 'v3.2.4' of https://github.com/ZhuLinsen/daily_stock_analysis +88fdaf79332a1d096ac5a647c1d89229abfd2327 not-for-merge tag 'v3.2.5' of https://github.com/ZhuLinsen/daily_stock_analysis +879b25950a8523603c8c4202e09df6d87bb9d1b0 not-for-merge tag 'v3.2.6' of https://github.com/ZhuLinsen/daily_stock_analysis +32162ff7455baa3ffc10241d5a7c0346a5288ace not-for-merge tag 'v3.2.7' of https://github.com/ZhuLinsen/daily_stock_analysis +0780cfbd7d1296d0345ae71510173adfb4bce61c not-for-merge tag 'v3.2.8' of https://github.com/ZhuLinsen/daily_stock_analysis +1e0a525fb2e87a5c9d93ced3cc3166bc32a09bae not-for-merge tag 'v3.2.9' of https://github.com/ZhuLinsen/daily_stock_analysis +5acd1c63a7b34720c7cd9439f002f824daa17f19 not-for-merge tag 'v3.3.0' of https://github.com/ZhuLinsen/daily_stock_analysis +d40ff8893ee0764f056f90192087149e83da8b34 not-for-merge tag 'v3.3.1' of https://github.com/ZhuLinsen/daily_stock_analysis +f42884638b371c32c8043f0b175941f6d0e471a5 not-for-merge tag 'v3.3.10' of https://github.com/ZhuLinsen/daily_stock_analysis +4e030c39589c495e70521fbff905b9fb0971d904 not-for-merge tag 'v3.3.11' of https://github.com/ZhuLinsen/daily_stock_analysis +c66fd3cf13b5b2e86791a5e2f50a124194d7ed83 not-for-merge tag 'v3.3.12' of https://github.com/ZhuLinsen/daily_stock_analysis +026630adb40efa7cd3687c8bc9e1260e3d8ad8b2 not-for-merge tag 'v3.3.13' of https://github.com/ZhuLinsen/daily_stock_analysis +75896f6d05c8e1f6d996ae92b45a36d37e935bbd not-for-merge tag 'v3.3.14' of https://github.com/ZhuLinsen/daily_stock_analysis +91a5c84c932c75f81098e53f765f022593c293c6 not-for-merge tag 'v3.3.15' of https://github.com/ZhuLinsen/daily_stock_analysis +e1695eccb060349b1bd52567a01abd112d7cbf3e not-for-merge tag 'v3.3.16' of https://github.com/ZhuLinsen/daily_stock_analysis +b1e6f03796c82b93748ac190970f18ba3434aee8 not-for-merge tag 'v3.3.17' of https://github.com/ZhuLinsen/daily_stock_analysis +c2ac509f96b0f2f48f5ffd652fdec5bc9cca330d not-for-merge tag 'v3.3.18' of https://github.com/ZhuLinsen/daily_stock_analysis +29a22af1f3d79c8e39fbd52d933952427613e7ef not-for-merge tag 'v3.3.19' of https://github.com/ZhuLinsen/daily_stock_analysis +39848b80a1bd0c862e4dc09ce30a716bcb925afe not-for-merge tag 'v3.3.2' of https://github.com/ZhuLinsen/daily_stock_analysis +b2326e9439f613b3466f340924e714355c2457bc not-for-merge tag 'v3.3.20' of https://github.com/ZhuLinsen/daily_stock_analysis +605224bd5a22ac521dbb581252065dac63f70926 not-for-merge tag 'v3.3.21' of https://github.com/ZhuLinsen/daily_stock_analysis +b0986655a952c871e0c82c31ab0825e658d27a95 not-for-merge tag 'v3.3.22' of https://github.com/ZhuLinsen/daily_stock_analysis +4bc362baf6aae35e64675dd1cc3e10bb9853dacc not-for-merge tag 'v3.3.23' of https://github.com/ZhuLinsen/daily_stock_analysis +036ca1c508bdc5cebd10d86267508e4cd730384f not-for-merge tag 'v3.3.24' of https://github.com/ZhuLinsen/daily_stock_analysis +ddd6aaa8e1f00721bec2661058b57b9166f1a95d not-for-merge tag 'v3.3.25' of https://github.com/ZhuLinsen/daily_stock_analysis +38b399143e48ebcac29613fe615d65ecd7265a1b not-for-merge tag 'v3.3.26' of https://github.com/ZhuLinsen/daily_stock_analysis +629e311d654726c86d628457296df8c745e66b5e not-for-merge tag 'v3.3.27' of https://github.com/ZhuLinsen/daily_stock_analysis +9249de1e53db2b54a67339cf28c5bedea2b8d89c not-for-merge tag 'v3.3.28' of https://github.com/ZhuLinsen/daily_stock_analysis +2349ac90c70f6e208d6f3e71c043bdbdcf333f40 not-for-merge tag 'v3.3.29' of https://github.com/ZhuLinsen/daily_stock_analysis +c7a670a31c67c2d5e28244b2b577e9a664241fa8 not-for-merge tag 'v3.3.3' of https://github.com/ZhuLinsen/daily_stock_analysis +115811e319d9509ecf1dbbe072d10830ca09f4ac not-for-merge tag 'v3.3.30' of https://github.com/ZhuLinsen/daily_stock_analysis +46a8af487db44cce47afce1c9dc9d82ad03cef2f not-for-merge tag 'v3.3.31' of https://github.com/ZhuLinsen/daily_stock_analysis +69752b6855512084fa45f168c63456ceba204414 not-for-merge tag 'v3.3.32' of https://github.com/ZhuLinsen/daily_stock_analysis +292c638f3a74fac4b3f1a35560fa5b02cc15a111 not-for-merge tag 'v3.3.4' of https://github.com/ZhuLinsen/daily_stock_analysis +0e2e11154581d80ab53ae9a7eb4c1b1ef1e24fdc not-for-merge tag 'v3.3.5' of https://github.com/ZhuLinsen/daily_stock_analysis +84260ee690fae17afa9e66902c9a5af0f41a3efe not-for-merge tag 'v3.3.6' of https://github.com/ZhuLinsen/daily_stock_analysis +3e1e8ea86edbdb6dbb13e8645b9bffab1062a148 not-for-merge tag 'v3.3.7' of https://github.com/ZhuLinsen/daily_stock_analysis +80907150357f0f550efdfee24f848851b542a08b not-for-merge tag 'v3.3.8' of https://github.com/ZhuLinsen/daily_stock_analysis +17402ce960ab7b57d7f809ed2741f725a50335e4 not-for-merge tag 'v3.3.9' of https://github.com/ZhuLinsen/daily_stock_analysis +0154992e18f6a5a09199a151ee75661e78b9c12f not-for-merge tag 'v3.4.0' of https://github.com/ZhuLinsen/daily_stock_analysis +c644e0144ee7a54c7a671b13ba3851a2036cc772 not-for-merge tag 'v3.4.1' of https://github.com/ZhuLinsen/daily_stock_analysis +b504a6e08783d181ba845cfed08d1b1e60d50615 not-for-merge tag 'v3.4.10' of https://github.com/ZhuLinsen/daily_stock_analysis +c4e94203ff5ab49bcf781faaca509ea02cea6d2d not-for-merge tag 'v3.4.11' of https://github.com/ZhuLinsen/daily_stock_analysis +a67933e226d04da7538066f3376bed89ab43c22d not-for-merge tag 'v3.4.2' of https://github.com/ZhuLinsen/daily_stock_analysis +b77519fbfbd69c13cbdbf977106bff796f7a508e not-for-merge tag 'v3.4.3' of https://github.com/ZhuLinsen/daily_stock_analysis +4a6cd534ac35d68e162cd4cbbfaf3b4e16763997 not-for-merge tag 'v3.4.4' of https://github.com/ZhuLinsen/daily_stock_analysis +1fe967d4b174a433270929decd24a61968124c6a not-for-merge tag 'v3.4.5' of https://github.com/ZhuLinsen/daily_stock_analysis +c3242896baa3288ee33f3a8bd69cb37f2e22ac4f not-for-merge tag 'v3.4.6' of https://github.com/ZhuLinsen/daily_stock_analysis +24d55204d9556f7b26a06fd61c5b80856a200355 not-for-merge tag 'v3.4.7' of https://github.com/ZhuLinsen/daily_stock_analysis +fc16e99738737e821fb4996ae906a24968aab1f4 not-for-merge tag 'v3.4.8' of https://github.com/ZhuLinsen/daily_stock_analysis +3b49bf086a0deb70c06a54ce5104359350c775fe not-for-merge tag 'v3.4.9' of https://github.com/ZhuLinsen/daily_stock_analysis +1f74930b73fdd6eb425774a4010a1f762bbb5ee8 not-for-merge tag 'v3.5.0' of https://github.com/ZhuLinsen/daily_stock_analysis +0c66e5aac8c93f82fce7c66b1374657f216d3b23 not-for-merge tag 'v3.6.0' of https://github.com/ZhuLinsen/daily_stock_analysis +bd7b8bd50ebb96bc3c3292be5e667fd2ff3995ed not-for-merge tag 'v3.7.0' of https://github.com/ZhuLinsen/daily_stock_analysis +9cb0b6783383977606ab75f5e9764854594230e5 not-for-merge tag 'v3.8.0' of https://github.com/ZhuLinsen/daily_stock_analysis +62aa45fb4793c2a27841f602db5a5d32e249ede1 not-for-merge tag 'v3.9.0' of https://github.com/ZhuLinsen/daily_stock_analysis diff --git a/.git---/HEAD b/.git---/HEAD new file mode 100644 index 0000000..b870d82 --- /dev/null +++ b/.git---/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/.git---/ORIG_HEAD b/.git---/ORIG_HEAD new file mode 100644 index 0000000..61709ce --- /dev/null +++ b/.git---/ORIG_HEAD @@ -0,0 +1 @@ +5f21df5a9576ceece08fe2963cfd77bdbd493390 diff --git a/.git---/config b/.git---/config new file mode 100644 index 0000000..b53b018 --- /dev/null +++ b/.git---/config @@ -0,0 +1,14 @@ +[core] + repositoryformatversion = 0 + filemode = false + bare = false + logallrefupdates = true + symlinks = false + ignorecase = true +[remote "origin"] + url = https://github.com/ZhuLinsen/daily_stock_analysis.git + fetch = +refs/heads/*:refs/remotes/origin/* +[branch "main"] + remote = origin + merge = refs/heads/main + vscode-merge-base = origin/main diff --git a/.git---/description b/.git---/description new file mode 100644 index 0000000..498b267 --- /dev/null +++ b/.git---/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/.git---/hooks/applypatch-msg.sample b/.git---/hooks/applypatch-msg.sample new file mode 100644 index 0000000..a5d7b84 --- /dev/null +++ b/.git---/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/.git---/hooks/commit-msg.sample b/.git---/hooks/commit-msg.sample new file mode 100644 index 0000000..b58d118 --- /dev/null +++ b/.git---/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/.git---/hooks/fsmonitor-watchman.sample b/.git---/hooks/fsmonitor-watchman.sample new file mode 100644 index 0000000..14ed0aa --- /dev/null +++ b/.git---/hooks/fsmonitor-watchman.sample @@ -0,0 +1,173 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + } + my $query = <<" END"; + ["query", "$git_work_tree", { + "since": $last_update_token, + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/.git---/hooks/post-update.sample b/.git---/hooks/post-update.sample new file mode 100644 index 0000000..ec17ec1 --- /dev/null +++ b/.git---/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/.git---/hooks/pre-applypatch.sample b/.git---/hooks/pre-applypatch.sample new file mode 100644 index 0000000..4142082 --- /dev/null +++ b/.git---/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/.git---/hooks/pre-commit.sample b/.git---/hooks/pre-commit.sample new file mode 100644 index 0000000..e144712 --- /dev/null +++ b/.git---/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/.git---/hooks/pre-merge-commit.sample b/.git---/hooks/pre-merge-commit.sample new file mode 100644 index 0000000..399eab1 --- /dev/null +++ b/.git---/hooks/pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/.git---/hooks/pre-push.sample b/.git---/hooks/pre-push.sample new file mode 100644 index 0000000..4ce688d --- /dev/null +++ b/.git---/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/.git---/hooks/pre-rebase.sample b/.git---/hooks/pre-rebase.sample new file mode 100644 index 0000000..6cbef5c --- /dev/null +++ b/.git---/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/.git---/hooks/pre-receive.sample b/.git---/hooks/pre-receive.sample new file mode 100644 index 0000000..a1fd29e --- /dev/null +++ b/.git---/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/.git---/hooks/prepare-commit-msg.sample b/.git---/hooks/prepare-commit-msg.sample new file mode 100644 index 0000000..10fa14c --- /dev/null +++ b/.git---/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/.git---/hooks/push-to-checkout.sample b/.git---/hooks/push-to-checkout.sample new file mode 100644 index 0000000..af5a0c0 --- /dev/null +++ b/.git---/hooks/push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/.git---/index b/.git---/index new file mode 100644 index 0000000..713af3b Binary files /dev/null and b/.git---/index differ diff --git a/.git---/info/exclude b/.git---/info/exclude new file mode 100644 index 0000000..a5196d1 --- /dev/null +++ b/.git---/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/.git---/objects/pack/pack-6068b116a13fe9cf57658efca45588003f8730c6.idx b/.git---/objects/pack/pack-6068b116a13fe9cf57658efca45588003f8730c6.idx new file mode 100644 index 0000000..d560ffb Binary files /dev/null and b/.git---/objects/pack/pack-6068b116a13fe9cf57658efca45588003f8730c6.idx differ diff --git a/.git---/objects/pack/pack-6068b116a13fe9cf57658efca45588003f8730c6.pack b/.git---/objects/pack/pack-6068b116a13fe9cf57658efca45588003f8730c6.pack new file mode 100644 index 0000000..16ff204 Binary files /dev/null and b/.git---/objects/pack/pack-6068b116a13fe9cf57658efca45588003f8730c6.pack differ diff --git a/.git---/objects/pack/pack-849b295322dd5e5cebdeb63a602b3ffe8d8a5b18.idx b/.git---/objects/pack/pack-849b295322dd5e5cebdeb63a602b3ffe8d8a5b18.idx new file mode 100644 index 0000000..f159573 Binary files /dev/null and b/.git---/objects/pack/pack-849b295322dd5e5cebdeb63a602b3ffe8d8a5b18.idx differ diff --git a/.git---/objects/pack/pack-849b295322dd5e5cebdeb63a602b3ffe8d8a5b18.pack b/.git---/objects/pack/pack-849b295322dd5e5cebdeb63a602b3ffe8d8a5b18.pack new file mode 100644 index 0000000..5c7edba Binary files /dev/null and b/.git---/objects/pack/pack-849b295322dd5e5cebdeb63a602b3ffe8d8a5b18.pack differ diff --git a/.git---/packed-refs b/.git---/packed-refs new file mode 100644 index 0000000..b299b25 --- /dev/null +++ b/.git---/packed-refs @@ -0,0 +1,61 @@ +# pack-refs with: peeled fully-peeled sorted +7a556d2b099d38cef642fa8fe59f43c040db0b38 refs/remotes/origin/analysis/fix-docker-webui-port-env-221 +4d7aa88c7d919edb337777369d08592981eda108 refs/remotes/origin/bugfix/package-import-error +583d93d1b459dd1f43f25b8a6d1332cb2486603b refs/remotes/origin/chore/codeowners-default-owner +5aa702db5dcccba5c2a99fa5b929c2d0ec30100e refs/remotes/origin/docs/readme-sync-20260201 +5d5d2617af1c53a9af33eeb0af6ed57b57df3e16 refs/remotes/origin/fear/email-and-search-improvements +f2a03be0a7122674e88d4ea14bc8c7ed0dc90664 refs/remotes/origin/feat/etf-support-and-code-normalization +a509b61e77f21e327c117d3cda6d7dc6f845d154 refs/remotes/origin/feat/unified-webui +908dd1081f431a64758f1bc21c6b226b0ae6cab6 refs/remotes/origin/fix/etf-market-data-and-wechat-split +cd0df5627bc6c0e87d6303565a53d47cc04922c9 refs/remotes/origin/fix/report-quality-and-data-enhancements +15e82f266c9537588cd137ec04b9f18a5cdc4c4a refs/remotes/origin/fix/webui-telegram +5f21df5a9576ceece08fe2963cfd77bdbd493390 refs/remotes/origin/main +6f9e87c60c2621264a20d1a0137d601519ddca09 refs/remotes/origin/test0114 +1224b8ca6afacd40912fb956a30ca78939f58dbe refs/tags/v1.0.0 +^48ca4ee74271852d365cd77f28c22c35fc9f8a1d +73fc0920d1cdecc6411078f503869117f0473e7e refs/tags/v1.6.0 +fa91c1b662e789076dae0f853436380d227993a1 refs/tags/v2.0.0 +^7a7695d88a65f4a06cc6de1bdcefd62798376110 +373293ece4f773d4db8e6c9dacc5c4db8183a8c9 refs/tags/v2.1.0 +^0541c6f4082739c588e353741d02a932bf013f1a +6e0f7ef1b98765a74cd62dad7aaca48c13cea22e refs/tags/v2.1.1 +34c34f99a8b255809ceb0cd1a327e236408d7c37 refs/tags/v2.1.10 +5537853b1e8e15b5c2880642e6aff5102f9db90c refs/tags/v2.1.11 +9a60f94df15c57416f10a60989927711fe438b0f refs/tags/v2.1.12 +6c6eaf8c6ae2c12c536adc06bd7853fcc79e1f09 refs/tags/v2.1.13 +97b08a89094dfed4af4873fb5df60e91359bf48a refs/tags/v2.1.14 +77671cc6a2c100e4cc5db3657c9473ee77b3d74f refs/tags/v2.1.2 +b13ca62972ca1eb8001d60654155bcac8936e229 refs/tags/v2.1.3 +afcb9d1470cf5d8434c367825dcbc39181ae974c refs/tags/v2.1.4 +5bf23c28277eb592855933f69f0b3affbdcf0edc refs/tags/v2.1.5 +ea3ce3c8034cb553ed2f2ac97fada6ce658dc6c4 refs/tags/v2.1.6 +8ebad0ad215742fd87ffc9260dfb0bf97738d6ae refs/tags/v2.1.7 +ff5e4c3a98afe30e6456a597463bf6807abce1a3 refs/tags/v2.1.8 +27dfa3f397e22124fcb8fba9fedf9be5f3adf7c8 refs/tags/v2.1.9 +c6fc79d9a0eaadbcb8728be69e5406947b4ba307 refs/tags/v2.2.0 +b27ca5e7c46f565e7f4dc992fb53c42d8b24d5e4 refs/tags/v2.2.1 +acbde32e03960fc2a9c9aaa7f2e257cdf069b50b refs/tags/v2.2.2 +286f73275ec3c10bc562294e18106808fd4f79c3 refs/tags/v2.2.3 +01f866effc365e6d8c5255e27285af9b5e3db1e8 refs/tags/v2.2.4 +f976de75d938db938348e843918cf41cff545485 refs/tags/v2.2.5 +231eccdc9c68673b679dff6cbbdb3778578189a4 refs/tags/v2.3.0 +1e63e65ce09142a1e8ca181855b6ab7cd3e4c7f0 refs/tags/v2.3.1 +ccdbd64fb7669671a64a546de4e9729a77ab802b refs/tags/v2.3.2 +52917baa02210fb7911491fcf48ecbf3f70e5812 refs/tags/v3.0.0 +bb284ef976f32f3194d04e0b9d51e77f1f2ab526 refs/tags/v3.0.1 +276ec0a3c799fa0623d5ee8a8641fe49a50c0037 refs/tags/v3.0.2 +428f5371d21db403ae4b7b757222eb11e4a3fa4c refs/tags/v3.0.3 +dbe1cd7cae777a0db7380117ce0db090085cc874 refs/tags/v3.0.4 +8f30f9093dbad81c0b3014d05571373436aa412d refs/tags/v3.0.5 +0e47e8a73760165d7e2433e24b55d370e8a14af0 refs/tags/v3.0.6 +ed7228cc1956a18cf2a963f751d3cfad20f575a8 refs/tags/v3.0.7 +50b75b6db5674eb705e252f78ced702ded6f090f refs/tags/v3.0.8 +64a263da85cab91aefde7590671591e7349baad2 refs/tags/v3.0.9 +0fbef07b8588d42db4055e2334a05a84e54eefec refs/tags/v3.1.0 +aafbb5f2ba28a19e9b2e39438d82cd652a575939 refs/tags/v3.1.1 +4c791a2818427fe1705e117dbba3ece9cd8d1e4a refs/tags/v3.1.2 +6339cfc818b4ef306c7e9a4a73f78e8cccd3c21b refs/tags/v3.1.3 +184084e415b8b0a8c4f28a1958ac6a6e2bda6449 refs/tags/v3.1.4 +5883dc41bde4906c8b8d83b920d3fd1778ec1ee0 refs/tags/v3.1.5 +996aba1095b3f8c729a2518bdf0f8b862a80d589 refs/tags/v3.1.6 +5f21df5a9576ceece08fe2963cfd77bdbd493390 refs/tags/v3.1.7 diff --git a/.git---/refs/heads/main b/.git---/refs/heads/main new file mode 100644 index 0000000..61709ce --- /dev/null +++ b/.git---/refs/heads/main @@ -0,0 +1 @@ +5f21df5a9576ceece08fe2963cfd77bdbd493390 diff --git a/.git---/refs/remotes/origin/HEAD b/.git---/refs/remotes/origin/HEAD new file mode 100644 index 0000000..4b0a875 --- /dev/null +++ b/.git---/refs/remotes/origin/HEAD @@ -0,0 +1 @@ +ref: refs/remotes/origin/main diff --git a/.git---/refs/remotes/origin/autocode/issue-1027-feature-stocks-index-json-90-sto b/.git---/refs/remotes/origin/autocode/issue-1027-feature-stocks-index-json-90-sto new file mode 100644 index 0000000..5648c83 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-1027-feature-stocks-index-json-90-sto @@ -0,0 +1 @@ +613bf2bcfdcdf63fe22c537c8831ee9fc02d44e4 diff --git a/.git---/refs/remotes/origin/autocode/issue-1029-feature-report-language b/.git---/refs/remotes/origin/autocode/issue-1029-feature-report-language new file mode 100644 index 0000000..c824c3f --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-1029-feature-report-language @@ -0,0 +1 @@ +1840f112fb724e7ba7e8dbb3d35808b64a12b8d2 diff --git a/.git---/refs/remotes/origin/autocode/issue-594-feature b/.git---/refs/remotes/origin/autocode/issue-594-feature new file mode 100644 index 0000000..b74716e --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-594-feature @@ -0,0 +1 @@ +9990f21e8fa42d3d0c997e1bbdcc32fbdeabd5f0 diff --git a/.git---/refs/remotes/origin/autocode/issue-637-feature b/.git---/refs/remotes/origin/autocode/issue-637-feature new file mode 100644 index 0000000..a5441aa --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-637-feature @@ -0,0 +1 @@ +3f1cfa1902caf7233526b2b3a24e1a5f0462c51f diff --git a/.git---/refs/remotes/origin/autocode/issue-751-feature-windows-mac b/.git---/refs/remotes/origin/autocode/issue-751-feature-windows-mac new file mode 100644 index 0000000..bc94d7e --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-751-feature-windows-mac @@ -0,0 +1 @@ +01f72edee9fe0cbf0eb7dcefbea7ad5983ee54a2 diff --git a/.git---/refs/remotes/origin/autocode/issue-765-bug-docker-webui b/.git---/refs/remotes/origin/autocode/issue-765-bug-docker-webui new file mode 100644 index 0000000..ddbce4c --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-765-bug-docker-webui @@ -0,0 +1 @@ +8caf7445e90fe65d11b8418a808b1e668da7ad81 diff --git a/.git---/refs/remotes/origin/autocode/issue-773-bug b/.git---/refs/remotes/origin/autocode/issue-773-bug new file mode 100644 index 0000000..25f1ae7 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-773-bug @@ -0,0 +1 @@ +027272a1a0e1b6007b5f767128dacfac0d6717cd diff --git a/.git---/refs/remotes/origin/autocode/issue-792-task b/.git---/refs/remotes/origin/autocode/issue-792-task new file mode 100644 index 0000000..6762b2c --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-792-task @@ -0,0 +1 @@ +e08619621b3b6a82ff6e92ed8855c40d5943a8c0 diff --git a/.git---/refs/remotes/origin/autocode/issue-796-bug-docker-web b/.git---/refs/remotes/origin/autocode/issue-796-bug-docker-web new file mode 100644 index 0000000..1748a79 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-796-bug-docker-web @@ -0,0 +1 @@ +3bdad39fc336edb678603bc4b0e7ddb33c3cae98 diff --git a/.git---/refs/remotes/origin/autocode/issue-812-feature-ai b/.git---/refs/remotes/origin/autocode/issue-812-feature-ai new file mode 100644 index 0000000..2bfb11d --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-812-feature-ai @@ -0,0 +1 @@ +e7757b0db95a74f50d2d0b8b3e209a4e7e102b49 diff --git a/.git---/refs/remotes/origin/autocode/issue-819-bug b/.git---/refs/remotes/origin/autocode/issue-819-bug new file mode 100644 index 0000000..154ea6f --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-819-bug @@ -0,0 +1 @@ +84b635a6e04940f14f2421fefff42ba312a03307 diff --git a/.git---/refs/remotes/origin/autocode/issue-821-feature b/.git---/refs/remotes/origin/autocode/issue-821-feature new file mode 100644 index 0000000..cdef841 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-821-feature @@ -0,0 +1 @@ +7fbb389ec11c8eeb984ec4e9e73ce0582ce4360b diff --git a/.git---/refs/remotes/origin/autocode/issue-825-task b/.git---/refs/remotes/origin/autocode/issue-825-task new file mode 100644 index 0000000..c5c2b5a --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-825-task @@ -0,0 +1 @@ +d3aa23f0975b5472aa6dd934acc16f7569d19aa1 diff --git a/.git---/refs/remotes/origin/autocode/issue-826-feature-release-win-minimax b/.git---/refs/remotes/origin/autocode/issue-826-feature-release-win-minimax new file mode 100644 index 0000000..ae74d44 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-826-feature-release-win-minimax @@ -0,0 +1 @@ +2b5808f4683795ebffc02a95edab9d5195efd849 diff --git a/.git---/refs/remotes/origin/autocode/issue-827-feature b/.git---/refs/remotes/origin/autocode/issue-827-feature new file mode 100644 index 0000000..941e6cf --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-827-feature @@ -0,0 +1 @@ +8449670a5fe194e32cba254d62680da01496043e diff --git a/.git---/refs/remotes/origin/autocode/issue-830-feature-v1-models b/.git---/refs/remotes/origin/autocode/issue-830-feature-v1-models new file mode 100644 index 0000000..11dae3c --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-830-feature-v1-models @@ -0,0 +1 @@ +38596a2bbe2e2a32f071aaf850670fca03096135 diff --git a/.git---/refs/remotes/origin/autocode/issue-832-bug b/.git---/refs/remotes/origin/autocode/issue-832-bug new file mode 100644 index 0000000..494574f --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-832-bug @@ -0,0 +1 @@ +7dd3a226cb6329e0b359716c26c37418b01fc959 diff --git a/.git---/refs/remotes/origin/autocode/issue-839-bug b/.git---/refs/remotes/origin/autocode/issue-839-bug new file mode 100644 index 0000000..522282f --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-839-bug @@ -0,0 +1 @@ +e60cd15a2bcb63478f15c95184bc5ef9c2eb17fe diff --git a/.git---/refs/remotes/origin/autocode/issue-840-security-litellm-pypi-1-82-7-1-8 b/.git---/refs/remotes/origin/autocode/issue-840-security-litellm-pypi-1-82-7-1-8 new file mode 100644 index 0000000..3e6b8e0 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-840-security-litellm-pypi-1-82-7-1-8 @@ -0,0 +1 @@ +ea946fce0754734e4eb6043d064ec47f6bae38b7 diff --git a/.git---/refs/remotes/origin/autocode/issue-848-bug-the-job-has-exceeded-the-max b/.git---/refs/remotes/origin/autocode/issue-848-bug-the-job-has-exceeded-the-max new file mode 100644 index 0000000..4f95801 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-848-bug-the-job-has-exceeded-the-max @@ -0,0 +1 @@ +2c47d6ebd210d6c0f5b023b30a4a2c65d5e94505 diff --git a/.git---/refs/remotes/origin/autocode/issue-854-ollama b/.git---/refs/remotes/origin/autocode/issue-854-ollama new file mode 100644 index 0000000..f4a47fa --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-854-ollama @@ -0,0 +1 @@ +f49f2884d13042a3194d209eedd54c1a780eccf3 diff --git a/.git---/refs/remotes/origin/autocode/issue-860-bug-stock-group-email-group-gith b/.git---/refs/remotes/origin/autocode/issue-860-bug-stock-group-email-group-gith new file mode 100644 index 0000000..6318ef0 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-860-bug-stock-group-email-group-gith @@ -0,0 +1 @@ +cddb351e375dcde570341e96009cbb70e0262e51 diff --git a/.git---/refs/remotes/origin/autocode/issue-863-feature-change-the-max-tokens-fr b/.git---/refs/remotes/origin/autocode/issue-863-feature-change-the-max-tokens-fr new file mode 100644 index 0000000..63a41ef --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-863-feature-change-the-max-tokens-fr @@ -0,0 +1 @@ +7c1efc6e7652ce641cf0275906582f714ba5996d diff --git a/.git---/refs/remotes/origin/autocode/issue-865-feature-llm-stream b/.git---/refs/remotes/origin/autocode/issue-865-feature-llm-stream new file mode 100644 index 0000000..ee745ab --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-865-feature-llm-stream @@ -0,0 +1 @@ +9ae8b04a91be289bbe1906182d16894b4faca772 diff --git a/.git---/refs/remotes/origin/autocode/issue-872-feature-github-action-ai b/.git---/refs/remotes/origin/autocode/issue-872-feature-github-action-ai new file mode 100644 index 0000000..4fb5fa3 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-872-feature-github-action-ai @@ -0,0 +1 @@ +5e94755c88b879eff71f893a71368497d1572a72 diff --git a/.git---/refs/remotes/origin/autocode/issue-875-feature-litellm b/.git---/refs/remotes/origin/autocode/issue-875-feature-litellm new file mode 100644 index 0000000..d2c9bc5 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-875-feature-litellm @@ -0,0 +1 @@ +6b7ab16a111890734935973f9e92f443460908f7 diff --git a/.git---/refs/remotes/origin/autocode/issue-876-bug b/.git---/refs/remotes/origin/autocode/issue-876-bug new file mode 100644 index 0000000..2ef9cb0 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-876-bug @@ -0,0 +1 @@ +02c2ee45efc9f320f43b4942074b866fc2b54c9e diff --git a/.git---/refs/remotes/origin/autocode/issue-877-bug-info-llm-prompt-response b/.git---/refs/remotes/origin/autocode/issue-877-bug-info-llm-prompt-response new file mode 100644 index 0000000..9cbfa2a --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-877-bug-info-llm-prompt-response @@ -0,0 +1 @@ +16c9bab53ef82bc869090e47cf94ac3307662ba8 diff --git a/.git---/refs/remotes/origin/autocode/issue-878-bug-sqlite b/.git---/refs/remotes/origin/autocode/issue-878-bug-sqlite new file mode 100644 index 0000000..00d6461 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-878-bug-sqlite @@ -0,0 +1 @@ +778168f9de58d66721e013530f932197a1fe4af7 diff --git a/.git---/refs/remotes/origin/autocode/issue-880-bug b/.git---/refs/remotes/origin/autocode/issue-880-bug new file mode 100644 index 0000000..21752de --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-880-bug @@ -0,0 +1 @@ +7175dd29259de4084b8f477e4301a500911147a8 diff --git a/.git---/refs/remotes/origin/autocode/issue-881-bug b/.git---/refs/remotes/origin/autocode/issue-881-bug new file mode 100644 index 0000000..87743bb --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-881-bug @@ -0,0 +1 @@ +cc745ac641fe0e43b3e272669ca53b613b1c51b4 diff --git a/.git---/refs/remotes/origin/autocode/issue-882-feature-serpapi b/.git---/refs/remotes/origin/autocode/issue-882-feature-serpapi new file mode 100644 index 0000000..d9b662c --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-882-feature-serpapi @@ -0,0 +1 @@ +183af0af2520385e44f03bd05271b698794ba6b7 diff --git a/.git---/refs/remotes/origin/autocode/issue-883-docs-readme b/.git---/refs/remotes/origin/autocode/issue-883-docs-readme new file mode 100644 index 0000000..c9635c0 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-883-docs-readme @@ -0,0 +1 @@ +4b23a4b9986d53fc8531fc4c7e12436b684a5c94 diff --git a/.git---/refs/remotes/origin/autocode/issue-896-bug-discord-webhook b/.git---/refs/remotes/origin/autocode/issue-896-bug-discord-webhook new file mode 100644 index 0000000..cab43d4 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-896-bug-discord-webhook @@ -0,0 +1 @@ +44ca7f54afbc2d8eb2031fd0002ca8eda64f4b40 diff --git a/.git---/refs/remotes/origin/autocode/issue-901-task b/.git---/refs/remotes/origin/autocode/issue-901-task new file mode 100644 index 0000000..1f24fa9 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-901-task @@ -0,0 +1 @@ +3e31b26e72e4f599892597bb148a28bed5ae09cd diff --git a/.git---/refs/remotes/origin/autocode/issue-902-feature-clawbot b/.git---/refs/remotes/origin/autocode/issue-902-feature-clawbot new file mode 100644 index 0000000..473fb99 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-902-feature-clawbot @@ -0,0 +1 @@ +42e0e0be50e956372dedf6b212ce11cd01701f73 diff --git a/.git---/refs/remotes/origin/autocode/issue-940-bug-stock-name b/.git---/refs/remotes/origin/autocode/issue-940-bug-stock-name new file mode 100644 index 0000000..3dc0999 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-940-bug-stock-name @@ -0,0 +1 @@ +075cbf98e25b3ae38b779c6be9208005efb55964 diff --git a/.git---/refs/remotes/origin/autocode/issue-942-bug b/.git---/refs/remotes/origin/autocode/issue-942-bug new file mode 100644 index 0000000..6981daa --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-942-bug @@ -0,0 +1 @@ +3bdf9903b682bbcb5163c36c5880f5979c035038 diff --git a/.git---/refs/remotes/origin/autocode/issue-944-feature-webui-ui b/.git---/refs/remotes/origin/autocode/issue-944-feature-webui-ui new file mode 100644 index 0000000..00d66f9 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-944-feature-webui-ui @@ -0,0 +1 @@ +8e3e5c72407e0fbf0610948366de6aefef0d74ce diff --git a/.git---/refs/remotes/origin/autocode/issue-946-bug b/.git---/refs/remotes/origin/autocode/issue-946-bug new file mode 100644 index 0000000..219f3f9 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-946-bug @@ -0,0 +1 @@ +19cb475350d1176b69390c3a8baef0571be39969 diff --git a/.git---/refs/remotes/origin/autocode/issue-967-bug-sse-cancellederror-re-raise b/.git---/refs/remotes/origin/autocode/issue-967-bug-sse-cancellederror-re-raise new file mode 100644 index 0000000..eaf8aee --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-967-bug-sse-cancellederror-re-raise @@ -0,0 +1 @@ +f488e76cc1128218882df33fd28b11a396ef4194 diff --git a/.git---/refs/remotes/origin/autocode/issue-968-bug-bot-ask b/.git---/refs/remotes/origin/autocode/issue-968-bug-bot-ask new file mode 100644 index 0000000..358e8f0 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-968-bug-bot-ask @@ -0,0 +1 @@ +7b9c03c6401b3332e7e202e312f5c72630c2a7e0 diff --git a/.git---/refs/remotes/origin/autocode/issue-969-bug-agent-sse b/.git---/refs/remotes/origin/autocode/issue-969-bug-agent-sse new file mode 100644 index 0000000..6fbeac0 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-969-bug-agent-sse @@ -0,0 +1 @@ +f6a88b85826497ef1888b939974965851b627315 diff --git a/.git---/refs/remotes/origin/autocode/issue-970-bug-ask b/.git---/refs/remotes/origin/autocode/issue-970-bug-ask new file mode 100644 index 0000000..eff75b3 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-970-bug-ask @@ -0,0 +1 @@ +6cdbd682eb4c088e20cd1c2795fb766ff9087da0 diff --git a/.git---/refs/remotes/origin/autocode/issue-976-feature-bug b/.git---/refs/remotes/origin/autocode/issue-976-feature-bug new file mode 100644 index 0000000..e70ce55 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-976-feature-bug @@ -0,0 +1 @@ +fd47fc20b6af24d9eaa2acfa2703d2d2d81a0ee0 diff --git a/.git---/refs/remotes/origin/autocode/issue-983-bug b/.git---/refs/remotes/origin/autocode/issue-983-bug new file mode 100644 index 0000000..aa902d5 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-983-bug @@ -0,0 +1 @@ +660ff09fde3d88b0e0a4cded20b2f2a9c8ceaecc diff --git a/.git---/refs/remotes/origin/autocode/issue-996-bug b/.git---/refs/remotes/origin/autocode/issue-996-bug new file mode 100644 index 0000000..e843762 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-996-bug @@ -0,0 +1 @@ +ffc8e725ee17528da8d3c686bd40c1500025ec8e diff --git a/.git---/refs/remotes/origin/autocode/issue-998-bug-a b/.git---/refs/remotes/origin/autocode/issue-998-bug-a new file mode 100644 index 0000000..93dd138 --- /dev/null +++ b/.git---/refs/remotes/origin/autocode/issue-998-bug-a @@ -0,0 +1 @@ +3c997f4811f2c3d850dc8977c5cb036c74cae00e diff --git a/.git---/refs/remotes/origin/chore/ai-governance-hardening b/.git---/refs/remotes/origin/chore/ai-governance-hardening new file mode 100644 index 0000000..94b9510 --- /dev/null +++ b/.git---/refs/remotes/origin/chore/ai-governance-hardening @@ -0,0 +1 @@ +046a7166d7303ed17522418133c3fad778b40e91 diff --git a/.git---/refs/remotes/origin/chore/changelog-flat-unreleased-format b/.git---/refs/remotes/origin/chore/changelog-flat-unreleased-format new file mode 100644 index 0000000..e159b6f --- /dev/null +++ b/.git---/refs/remotes/origin/chore/changelog-flat-unreleased-format @@ -0,0 +1 @@ +fcf333798904385de4ad956663c63fafecb24a57 diff --git a/.git---/refs/remotes/origin/copilot/docs-simplify-readme-structure b/.git---/refs/remotes/origin/copilot/docs-simplify-readme-structure new file mode 100644 index 0000000..a3bd0be --- /dev/null +++ b/.git---/refs/remotes/origin/copilot/docs-simplify-readme-structure @@ -0,0 +1 @@ +e0d8ed566799dec2211a6bd52cdb8503cb897641 diff --git a/.git---/refs/remotes/origin/copilot/sub-pr-641 b/.git---/refs/remotes/origin/copilot/sub-pr-641 new file mode 100644 index 0000000..17727dd --- /dev/null +++ b/.git---/refs/remotes/origin/copilot/sub-pr-641 @@ -0,0 +1 @@ +26f2e2e162dc8c2aca605dfb17cb0b95792e0192 diff --git a/.git---/refs/remotes/origin/copilot/sub-pr-648 b/.git---/refs/remotes/origin/copilot/sub-pr-648 new file mode 100644 index 0000000..89e2205 --- /dev/null +++ b/.git---/refs/remotes/origin/copilot/sub-pr-648 @@ -0,0 +1 @@ +8ee34dbb1c4241cf0e705e17b9922eabd3257d86 diff --git a/.git---/refs/remotes/origin/copilot/validate-branch-content-and-issues b/.git---/refs/remotes/origin/copilot/validate-branch-content-and-issues new file mode 100644 index 0000000..891f483 --- /dev/null +++ b/.git---/refs/remotes/origin/copilot/validate-branch-content-and-issues @@ -0,0 +1 @@ +70227db11512d524138634e5d11734265ed0970f diff --git a/.git---/refs/remotes/origin/docs/governance-doc-sync-rules b/.git---/refs/remotes/origin/docs/governance-doc-sync-rules new file mode 100644 index 0000000..7364a90 --- /dev/null +++ b/.git---/refs/remotes/origin/docs/governance-doc-sync-rules @@ -0,0 +1 @@ +ae4aef3efa852fb40f258592556265c0774cb0f3 diff --git a/.git---/refs/remotes/origin/docs/i18n-improvements b/.git---/refs/remotes/origin/docs/i18n-improvements new file mode 100644 index 0000000..7ec87cc --- /dev/null +++ b/.git---/refs/remotes/origin/docs/i18n-improvements @@ -0,0 +1 @@ +6c914ff91e434c7a823331ed08ed3e8d7b3b94a3 diff --git a/.git---/refs/remotes/origin/docs/trendshift-readme-badge b/.git---/refs/remotes/origin/docs/trendshift-readme-badge new file mode 100644 index 0000000..8c2c259 --- /dev/null +++ b/.git---/refs/remotes/origin/docs/trendshift-readme-badge @@ -0,0 +1 @@ +144e015a8073ade434709ae5288825b1c5e2b344 diff --git a/.git---/refs/remotes/origin/feat/779-skill-alignment b/.git---/refs/remotes/origin/feat/779-skill-alignment new file mode 100644 index 0000000..35ff107 --- /dev/null +++ b/.git---/refs/remotes/origin/feat/779-skill-alignment @@ -0,0 +1 @@ +dba9f3a3e4b8b419e7037726dfb1b1e8a46b3351 diff --git a/.git---/refs/remotes/origin/feat/ai-review-ci-context b/.git---/refs/remotes/origin/feat/ai-review-ci-context new file mode 100644 index 0000000..8a477cf --- /dev/null +++ b/.git---/refs/remotes/origin/feat/ai-review-ci-context @@ -0,0 +1 @@ +b91b01d5ec4dae06eeec1fca1201fe5f60147152 diff --git a/.git---/refs/remotes/origin/feat/bot-commands-dispatch b/.git---/refs/remotes/origin/feat/bot-commands-dispatch new file mode 100644 index 0000000..0fd97ca --- /dev/null +++ b/.git---/refs/remotes/origin/feat/bot-commands-dispatch @@ -0,0 +1 @@ +9b9e73f0c09587385f4423043d7bb95dc01d6322 diff --git a/.git---/refs/remotes/origin/feat/deep-research-events b/.git---/refs/remotes/origin/feat/deep-research-events new file mode 100644 index 0000000..005abe6 --- /dev/null +++ b/.git---/refs/remotes/origin/feat/deep-research-events @@ -0,0 +1 @@ +2b152174b95f15c176485ce095b7f43388845333 diff --git a/.git---/refs/remotes/origin/feat/issue-669-related-boards b/.git---/refs/remotes/origin/feat/issue-669-related-boards new file mode 100644 index 0000000..5fb11fa --- /dev/null +++ b/.git---/refs/remotes/origin/feat/issue-669-related-boards @@ -0,0 +1 @@ +2c3d44ca9fc4a193358787d62b1d3b1e600bf45f diff --git a/.git---/refs/remotes/origin/feat/issue-754-desktop-env-backup b/.git---/refs/remotes/origin/feat/issue-754-desktop-env-backup new file mode 100644 index 0000000..81dfb7e --- /dev/null +++ b/.git---/refs/remotes/origin/feat/issue-754-desktop-env-backup @@ -0,0 +1 @@ +dd4c7184d1c8c13417bcb75eb75cb7cdd2201b42 diff --git a/.git---/refs/remotes/origin/feat/llm-config-usability b/.git---/refs/remotes/origin/feat/llm-config-usability new file mode 100644 index 0000000..324b4dc --- /dev/null +++ b/.git---/refs/remotes/origin/feat/llm-config-usability @@ -0,0 +1 @@ +6937a1521d782410ea50c16a982ffd58709aae2a diff --git a/.git---/refs/remotes/origin/feat/multi-agent-core b/.git---/refs/remotes/origin/feat/multi-agent-core new file mode 100644 index 0000000..ce93952 --- /dev/null +++ b/.git---/refs/remotes/origin/feat/multi-agent-core @@ -0,0 +1 @@ +28126db5bd7bcba9897063831f72dce1ccef6cf3 diff --git a/.git---/refs/remotes/origin/feat/multi-agents-arch b/.git---/refs/remotes/origin/feat/multi-agents-arch new file mode 100644 index 0000000..ad3a3cc --- /dev/null +++ b/.git---/refs/remotes/origin/feat/multi-agents-arch @@ -0,0 +1 @@ +b08e14012b5322701354155a37a0cb676e5713b3 diff --git a/.git---/refs/remotes/origin/feat/report-language-758 b/.git---/refs/remotes/origin/feat/report-language-758 new file mode 100644 index 0000000..43557ab --- /dev/null +++ b/.git---/refs/remotes/origin/feat/report-language-758 @@ -0,0 +1 @@ +9cd1d2f1ea96fb0dea6f2bbd0f1b6c5be16a1add diff --git a/.git---/refs/remotes/origin/fix-analysis-api-batch-contract b/.git---/refs/remotes/origin/fix-analysis-api-batch-contract new file mode 100644 index 0000000..5f7996e --- /dev/null +++ b/.git---/refs/remotes/origin/fix-analysis-api-batch-contract @@ -0,0 +1 @@ +50c4171736680da3c44cba9a5009e2ecb31bb7dc diff --git a/.git---/refs/remotes/origin/fix/agent-max-steps-ceiling b/.git---/refs/remotes/origin/fix/agent-max-steps-ceiling new file mode 100644 index 0000000..5744271 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/agent-max-steps-ceiling @@ -0,0 +1 @@ +a9af3957f3aab155d67cd0645336160cabcaabf0 diff --git a/.git---/refs/remotes/origin/fix/agent-min-budget-guard b/.git---/refs/remotes/origin/fix/agent-min-budget-guard new file mode 100644 index 0000000..c858d28 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/agent-min-budget-guard @@ -0,0 +1 @@ +e22278eb904543c5db177084488fcabb627cc101 diff --git a/.git---/refs/remotes/origin/fix/backend-ci-single-install b/.git---/refs/remotes/origin/fix/backend-ci-single-install new file mode 100644 index 0000000..ac6057c --- /dev/null +++ b/.git---/refs/remotes/origin/fix/backend-ci-single-install @@ -0,0 +1 @@ +0f49c461f2d95af02b780eebd48d5871a278d47d diff --git a/.git---/refs/remotes/origin/fix/daily-analysis-timeout-increase b/.git---/refs/remotes/origin/fix/daily-analysis-timeout-increase new file mode 100644 index 0000000..7116082 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/daily-analysis-timeout-increase @@ -0,0 +1 @@ +b30873901ed74126648eabebb919b8a6eb8effab diff --git a/.git---/refs/remotes/origin/fix/email-sender-name-encoding-708 b/.git---/refs/remotes/origin/fix/email-sender-name-encoding-708 new file mode 100644 index 0000000..b79109c --- /dev/null +++ b/.git---/refs/remotes/origin/fix/email-sender-name-encoding-708 @@ -0,0 +1 @@ +283f01306255ef2276fe90dc5391faaddbb13ec1 diff --git a/.git---/refs/remotes/origin/fix/github-actions-node24-upgrade b/.git---/refs/remotes/origin/fix/github-actions-node24-upgrade new file mode 100644 index 0000000..3145c66 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/github-actions-node24-upgrade @@ -0,0 +1 @@ +ffbe1e178a792597222ac6084bde0a270a633806 diff --git a/.git---/refs/remotes/origin/fix/hk-stock-code-629-691 b/.git---/refs/remotes/origin/fix/hk-stock-code-629-691 new file mode 100644 index 0000000..334b0ac --- /dev/null +++ b/.git---/refs/remotes/origin/fix/hk-stock-code-629-691 @@ -0,0 +1 @@ +362a7bb9b1d6c894de2cfdd507d499d5605b4231 diff --git a/.git---/refs/remotes/origin/fix/issue-726-schedule-immediate-compat b/.git---/refs/remotes/origin/fix/issue-726-schedule-immediate-compat new file mode 100644 index 0000000..06c0a26 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/issue-726-schedule-immediate-compat @@ -0,0 +1 @@ +b318bcdacc6f04edb7c0a3725d4909e12eb7abcc diff --git a/.git---/refs/remotes/origin/fix/issue-749-copy-button b/.git---/refs/remotes/origin/fix/issue-749-copy-button new file mode 100644 index 0000000..9cba310 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/issue-749-copy-button @@ -0,0 +1 @@ +d411b96c0f9c8457f6471b223cd9519efd0c781e diff --git a/.git---/refs/remotes/origin/fix/issue-772-portfolio-fx-refresh-disabled b/.git---/refs/remotes/origin/fix/issue-772-portfolio-fx-refresh-disabled new file mode 100644 index 0000000..96cfccb --- /dev/null +++ b/.git---/refs/remotes/origin/fix/issue-772-portfolio-fx-refresh-disabled @@ -0,0 +1 @@ +666665a78b681ba35241b3d607860624b9eb1088 diff --git a/.git---/refs/remotes/origin/fix/issue-782-tavily-news-filter b/.git---/refs/remotes/origin/fix/issue-782-tavily-news-filter new file mode 100644 index 0000000..7fb34c6 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/issue-782-tavily-news-filter @@ -0,0 +1 @@ +e9a050a2ef1f359061d1c3e7466183f89dd87e87 diff --git a/.git---/refs/remotes/origin/fix/llm-rate-limit-detection b/.git---/refs/remotes/origin/fix/llm-rate-limit-detection new file mode 100644 index 0000000..8b19c18 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/llm-rate-limit-detection @@ -0,0 +1 @@ +a6fc1c53fb5d395863906550c2989fb481e5c8ca diff --git a/.git---/refs/remotes/origin/fix/multi-agent-max-steps-and-graceful-degradation b/.git---/refs/remotes/origin/fix/multi-agent-max-steps-and-graceful-degradation new file mode 100644 index 0000000..96b3900 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/multi-agent-max-steps-and-graceful-degradation @@ -0,0 +1 @@ +e4009e16f861ceac8a854e1d20611e2cce4cc93e diff --git a/.git---/refs/remotes/origin/fix/p0-stability-hardening b/.git---/refs/remotes/origin/fix/p0-stability-hardening new file mode 100644 index 0000000..db68ae1 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/p0-stability-hardening @@ -0,0 +1 @@ +229d6a3aa3dc4115f781aefdd0f0735682301aee diff --git a/.git---/refs/remotes/origin/fix/pipeline-optional-service-resilience b/.git---/refs/remotes/origin/fix/pipeline-optional-service-resilience new file mode 100644 index 0000000..f025a9d --- /dev/null +++ b/.git---/refs/remotes/origin/fix/pipeline-optional-service-resilience @@ -0,0 +1 @@ +68ff8eaaba44d6da3359c6f38df3ef7573c0844f diff --git a/.git---/refs/remotes/origin/fix/post-merge-auth-tushare-followups b/.git---/refs/remotes/origin/fix/post-merge-auth-tushare-followups new file mode 100644 index 0000000..ea9783f --- /dev/null +++ b/.git---/refs/remotes/origin/fix/post-merge-auth-tushare-followups @@ -0,0 +1 @@ +24e011636d434f2fb610bf74555c5bb99afb8d7d diff --git a/.git---/refs/remotes/origin/fix/realtime-indicator-date-tz-test b/.git---/refs/remotes/origin/fix/realtime-indicator-date-tz-test new file mode 100644 index 0000000..2a9b60e --- /dev/null +++ b/.git---/refs/remotes/origin/fix/realtime-indicator-date-tz-test @@ -0,0 +1 @@ +8db6e3870abea74362ef89d69a2adb5093e8639e diff --git a/.git---/refs/remotes/origin/fix/recent-bug-triage b/.git---/refs/remotes/origin/fix/recent-bug-triage new file mode 100644 index 0000000..3337106 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/recent-bug-triage @@ -0,0 +1 @@ +8711ab653bd7192f23cb566f27fe1231df686992 diff --git a/.git---/refs/remotes/origin/fix/recover-pr-648-649 b/.git---/refs/remotes/origin/fix/recover-pr-648-649 new file mode 100644 index 0000000..c6867cb --- /dev/null +++ b/.git---/refs/remotes/origin/fix/recover-pr-648-649 @@ -0,0 +1 @@ +0372479645b6d72d9088d4ec12f07c102bd978f4 diff --git a/.git---/refs/remotes/origin/fix/report-details-copy-feedback-pr b/.git---/refs/remotes/origin/fix/report-details-copy-feedback-pr new file mode 100644 index 0000000..2d7bd74 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/report-details-copy-feedback-pr @@ -0,0 +1 @@ +af0b67cc04006d176d6da1d0a91d38c2f4027347 diff --git a/.git---/refs/remotes/origin/fix/report-language-workflow-and-status-price b/.git---/refs/remotes/origin/fix/report-language-workflow-and-status-price new file mode 100644 index 0000000..71f4ed2 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/report-language-workflow-and-status-price @@ -0,0 +1 @@ +92a5f68fb1eaf106adf606b7253f4b1f6a219905 diff --git a/.git---/refs/remotes/origin/fix/restore-pypi-litellm b/.git---/refs/remotes/origin/fix/restore-pypi-litellm new file mode 100644 index 0000000..3e256b8 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/restore-pypi-litellm @@ -0,0 +1 @@ +08bb61b9d11366c435bc22388614bc8f98f0fee1 diff --git a/.git---/refs/remotes/origin/fix/runtime-robustness b/.git---/refs/remotes/origin/fix/runtime-robustness new file mode 100644 index 0000000..ca55578 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/runtime-robustness @@ -0,0 +1 @@ +77e37214a662f1e5061bfbee3e3925f5ecc37a51 diff --git a/.git---/refs/remotes/origin/fix/skill-compat-followups b/.git---/refs/remotes/origin/fix/skill-compat-followups new file mode 100644 index 0000000..f0ce266 --- /dev/null +++ b/.git---/refs/remotes/origin/fix/skill-compat-followups @@ -0,0 +1 @@ +3c378308b48b6f9b19a4a590805d396cf668c427 diff --git a/.git---/refs/remotes/origin/fix/trading-philosophy-injection-complete b/.git---/refs/remotes/origin/fix/trading-philosophy-injection-complete new file mode 100644 index 0000000..e5db26f --- /dev/null +++ b/.git---/refs/remotes/origin/fix/trading-philosophy-injection-complete @@ -0,0 +1 @@ +07c8e19135f805ccd6f51913a077a65642f2073e diff --git a/.git---/refs/remotes/origin/fix/tushare-http-client-init b/.git---/refs/remotes/origin/fix/tushare-http-client-init new file mode 100644 index 0000000..4f5582c --- /dev/null +++ b/.git---/refs/remotes/origin/fix/tushare-http-client-init @@ -0,0 +1 @@ +528d39411253b6deb18d5da49d7a6d2ae607263f diff --git a/.git---/refs/remotes/origin/main b/.git---/refs/remotes/origin/main new file mode 100644 index 0000000..fd532e5 --- /dev/null +++ b/.git---/refs/remotes/origin/main @@ -0,0 +1 @@ +3dceb1eca8487e5763b5995f78dd9d723213423d diff --git a/.git---/refs/remotes/origin/pr-919 b/.git---/refs/remotes/origin/pr-919 new file mode 100644 index 0000000..09423fa --- /dev/null +++ b/.git---/refs/remotes/origin/pr-919 @@ -0,0 +1 @@ +8db149f1b0da2258ec15ba8e5a101f6785c297ff diff --git a/.git---/refs/tags/v3.1.10 b/.git---/refs/tags/v3.1.10 new file mode 100644 index 0000000..9fa5adf --- /dev/null +++ b/.git---/refs/tags/v3.1.10 @@ -0,0 +1 @@ +2692b20c2d5a372444c436652ea66998262cb1f6 diff --git a/.git---/refs/tags/v3.1.11 b/.git---/refs/tags/v3.1.11 new file mode 100644 index 0000000..3f7298d --- /dev/null +++ b/.git---/refs/tags/v3.1.11 @@ -0,0 +1 @@ +b19a4d0df9c040907965ab177df5f775ecdf9aa2 diff --git a/.git---/refs/tags/v3.1.12 b/.git---/refs/tags/v3.1.12 new file mode 100644 index 0000000..2218afd --- /dev/null +++ b/.git---/refs/tags/v3.1.12 @@ -0,0 +1 @@ +542c8e4314fe2955a4050f56f2d2c93df8e15690 diff --git a/.git---/refs/tags/v3.1.13 b/.git---/refs/tags/v3.1.13 new file mode 100644 index 0000000..50b592c --- /dev/null +++ b/.git---/refs/tags/v3.1.13 @@ -0,0 +1 @@ +5414a6de80442388b1c7b0eb0ae3a8aba3d7dfa1 diff --git a/.git---/refs/tags/v3.1.8 b/.git---/refs/tags/v3.1.8 new file mode 100644 index 0000000..1b69d78 --- /dev/null +++ b/.git---/refs/tags/v3.1.8 @@ -0,0 +1 @@ +84a3da99533bdf9f994f81fd4bb2f6dfb4d43091 diff --git a/.git---/refs/tags/v3.1.9 b/.git---/refs/tags/v3.1.9 new file mode 100644 index 0000000..414a1d4 --- /dev/null +++ b/.git---/refs/tags/v3.1.9 @@ -0,0 +1 @@ +6a7c08d52ed056b75f11e8dd00a341fa88df478a diff --git a/.git---/refs/tags/v3.10.0 b/.git---/refs/tags/v3.10.0 new file mode 100644 index 0000000..059540f --- /dev/null +++ b/.git---/refs/tags/v3.10.0 @@ -0,0 +1 @@ +edd51c858d06d68d964f1c9c5f651b4a827b142d diff --git a/.git---/refs/tags/v3.10.1 b/.git---/refs/tags/v3.10.1 new file mode 100644 index 0000000..613bfc9 --- /dev/null +++ b/.git---/refs/tags/v3.10.1 @@ -0,0 +1 @@ +4914a95cab165b19e183ff37dbbecdb42ce8f9eb diff --git a/.git---/refs/tags/v3.11.0 b/.git---/refs/tags/v3.11.0 new file mode 100644 index 0000000..072b9f3 --- /dev/null +++ b/.git---/refs/tags/v3.11.0 @@ -0,0 +1 @@ +863c268053d3a65b65c9c3a8d7f444d026e584c3 diff --git a/.git---/refs/tags/v3.12.0 b/.git---/refs/tags/v3.12.0 new file mode 100644 index 0000000..e590ef2 --- /dev/null +++ b/.git---/refs/tags/v3.12.0 @@ -0,0 +1 @@ +d1bb085c7cafef762b38b78e67984a7eaf1a237b diff --git a/.git---/refs/tags/v3.2.0 b/.git---/refs/tags/v3.2.0 new file mode 100644 index 0000000..85fe083 --- /dev/null +++ b/.git---/refs/tags/v3.2.0 @@ -0,0 +1 @@ +498770616f43ef3a554e249b87be7372928f1aa7 diff --git a/.git---/refs/tags/v3.2.1 b/.git---/refs/tags/v3.2.1 new file mode 100644 index 0000000..7d3edca --- /dev/null +++ b/.git---/refs/tags/v3.2.1 @@ -0,0 +1 @@ +b8ca520f195b98421b4b86e44118d2a939fb6fbe diff --git a/.git---/refs/tags/v3.2.10 b/.git---/refs/tags/v3.2.10 new file mode 100644 index 0000000..ebfcd7e --- /dev/null +++ b/.git---/refs/tags/v3.2.10 @@ -0,0 +1 @@ +e780f840a33c1adc78e92a8392789029ac37fb87 diff --git a/.git---/refs/tags/v3.2.11 b/.git---/refs/tags/v3.2.11 new file mode 100644 index 0000000..b8b0a1f --- /dev/null +++ b/.git---/refs/tags/v3.2.11 @@ -0,0 +1 @@ +c9b01fead240b7ccbecdd0448350ac32feb45622 diff --git a/.git---/refs/tags/v3.2.2 b/.git---/refs/tags/v3.2.2 new file mode 100644 index 0000000..6002fa6 --- /dev/null +++ b/.git---/refs/tags/v3.2.2 @@ -0,0 +1 @@ +7cf19251798a01238f8bc681f1cf24ccb83f0131 diff --git a/.git---/refs/tags/v3.2.3 b/.git---/refs/tags/v3.2.3 new file mode 100644 index 0000000..66d8c78 --- /dev/null +++ b/.git---/refs/tags/v3.2.3 @@ -0,0 +1 @@ +648bf5b6384a2cf3bb6bf53316979b9d1d0b03c5 diff --git a/.git---/refs/tags/v3.2.4 b/.git---/refs/tags/v3.2.4 new file mode 100644 index 0000000..deb4094 --- /dev/null +++ b/.git---/refs/tags/v3.2.4 @@ -0,0 +1 @@ +9955b8ab520b8154111dd251aa65d9ba5c0b8d4a diff --git a/.git---/refs/tags/v3.2.5 b/.git---/refs/tags/v3.2.5 new file mode 100644 index 0000000..3b401f8 --- /dev/null +++ b/.git---/refs/tags/v3.2.5 @@ -0,0 +1 @@ +88fdaf79332a1d096ac5a647c1d89229abfd2327 diff --git a/.git---/refs/tags/v3.2.6 b/.git---/refs/tags/v3.2.6 new file mode 100644 index 0000000..30c347e --- /dev/null +++ b/.git---/refs/tags/v3.2.6 @@ -0,0 +1 @@ +879b25950a8523603c8c4202e09df6d87bb9d1b0 diff --git a/.git---/refs/tags/v3.2.7 b/.git---/refs/tags/v3.2.7 new file mode 100644 index 0000000..cc5be89 --- /dev/null +++ b/.git---/refs/tags/v3.2.7 @@ -0,0 +1 @@ +32162ff7455baa3ffc10241d5a7c0346a5288ace diff --git a/.git---/refs/tags/v3.2.8 b/.git---/refs/tags/v3.2.8 new file mode 100644 index 0000000..58e2bb1 --- /dev/null +++ b/.git---/refs/tags/v3.2.8 @@ -0,0 +1 @@ +0780cfbd7d1296d0345ae71510173adfb4bce61c diff --git a/.git---/refs/tags/v3.2.9 b/.git---/refs/tags/v3.2.9 new file mode 100644 index 0000000..497fcfd --- /dev/null +++ b/.git---/refs/tags/v3.2.9 @@ -0,0 +1 @@ +1e0a525fb2e87a5c9d93ced3cc3166bc32a09bae diff --git a/.git---/refs/tags/v3.3.0 b/.git---/refs/tags/v3.3.0 new file mode 100644 index 0000000..dabd38a --- /dev/null +++ b/.git---/refs/tags/v3.3.0 @@ -0,0 +1 @@ +5acd1c63a7b34720c7cd9439f002f824daa17f19 diff --git a/.git---/refs/tags/v3.3.1 b/.git---/refs/tags/v3.3.1 new file mode 100644 index 0000000..490f769 --- /dev/null +++ b/.git---/refs/tags/v3.3.1 @@ -0,0 +1 @@ +d40ff8893ee0764f056f90192087149e83da8b34 diff --git a/.git---/refs/tags/v3.3.10 b/.git---/refs/tags/v3.3.10 new file mode 100644 index 0000000..3acae93 --- /dev/null +++ b/.git---/refs/tags/v3.3.10 @@ -0,0 +1 @@ +f42884638b371c32c8043f0b175941f6d0e471a5 diff --git a/.git---/refs/tags/v3.3.11 b/.git---/refs/tags/v3.3.11 new file mode 100644 index 0000000..6804ba2 --- /dev/null +++ b/.git---/refs/tags/v3.3.11 @@ -0,0 +1 @@ +4e030c39589c495e70521fbff905b9fb0971d904 diff --git a/.git---/refs/tags/v3.3.12 b/.git---/refs/tags/v3.3.12 new file mode 100644 index 0000000..a30d307 --- /dev/null +++ b/.git---/refs/tags/v3.3.12 @@ -0,0 +1 @@ +c66fd3cf13b5b2e86791a5e2f50a124194d7ed83 diff --git a/.git---/refs/tags/v3.3.13 b/.git---/refs/tags/v3.3.13 new file mode 100644 index 0000000..ebacd4f --- /dev/null +++ b/.git---/refs/tags/v3.3.13 @@ -0,0 +1 @@ +026630adb40efa7cd3687c8bc9e1260e3d8ad8b2 diff --git a/.git---/refs/tags/v3.3.14 b/.git---/refs/tags/v3.3.14 new file mode 100644 index 0000000..c5132d9 --- /dev/null +++ b/.git---/refs/tags/v3.3.14 @@ -0,0 +1 @@ +75896f6d05c8e1f6d996ae92b45a36d37e935bbd diff --git a/.git---/refs/tags/v3.3.15 b/.git---/refs/tags/v3.3.15 new file mode 100644 index 0000000..767647a --- /dev/null +++ b/.git---/refs/tags/v3.3.15 @@ -0,0 +1 @@ +91a5c84c932c75f81098e53f765f022593c293c6 diff --git a/.git---/refs/tags/v3.3.16 b/.git---/refs/tags/v3.3.16 new file mode 100644 index 0000000..c9e96e1 --- /dev/null +++ b/.git---/refs/tags/v3.3.16 @@ -0,0 +1 @@ +e1695eccb060349b1bd52567a01abd112d7cbf3e diff --git a/.git---/refs/tags/v3.3.17 b/.git---/refs/tags/v3.3.17 new file mode 100644 index 0000000..d551217 --- /dev/null +++ b/.git---/refs/tags/v3.3.17 @@ -0,0 +1 @@ +b1e6f03796c82b93748ac190970f18ba3434aee8 diff --git a/.git---/refs/tags/v3.3.18 b/.git---/refs/tags/v3.3.18 new file mode 100644 index 0000000..22fcbaf --- /dev/null +++ b/.git---/refs/tags/v3.3.18 @@ -0,0 +1 @@ +c2ac509f96b0f2f48f5ffd652fdec5bc9cca330d diff --git a/.git---/refs/tags/v3.3.19 b/.git---/refs/tags/v3.3.19 new file mode 100644 index 0000000..2da0a8a --- /dev/null +++ b/.git---/refs/tags/v3.3.19 @@ -0,0 +1 @@ +29a22af1f3d79c8e39fbd52d933952427613e7ef diff --git a/.git---/refs/tags/v3.3.2 b/.git---/refs/tags/v3.3.2 new file mode 100644 index 0000000..8022991 --- /dev/null +++ b/.git---/refs/tags/v3.3.2 @@ -0,0 +1 @@ +39848b80a1bd0c862e4dc09ce30a716bcb925afe diff --git a/.git---/refs/tags/v3.3.20 b/.git---/refs/tags/v3.3.20 new file mode 100644 index 0000000..a3f27be --- /dev/null +++ b/.git---/refs/tags/v3.3.20 @@ -0,0 +1 @@ +b2326e9439f613b3466f340924e714355c2457bc diff --git a/.git---/refs/tags/v3.3.21 b/.git---/refs/tags/v3.3.21 new file mode 100644 index 0000000..aed9d7c --- /dev/null +++ b/.git---/refs/tags/v3.3.21 @@ -0,0 +1 @@ +605224bd5a22ac521dbb581252065dac63f70926 diff --git a/.git---/refs/tags/v3.3.22 b/.git---/refs/tags/v3.3.22 new file mode 100644 index 0000000..15c57e0 --- /dev/null +++ b/.git---/refs/tags/v3.3.22 @@ -0,0 +1 @@ +b0986655a952c871e0c82c31ab0825e658d27a95 diff --git a/.git---/refs/tags/v3.3.23 b/.git---/refs/tags/v3.3.23 new file mode 100644 index 0000000..a232e32 --- /dev/null +++ b/.git---/refs/tags/v3.3.23 @@ -0,0 +1 @@ +4bc362baf6aae35e64675dd1cc3e10bb9853dacc diff --git a/.git---/refs/tags/v3.3.24 b/.git---/refs/tags/v3.3.24 new file mode 100644 index 0000000..0861463 --- /dev/null +++ b/.git---/refs/tags/v3.3.24 @@ -0,0 +1 @@ +036ca1c508bdc5cebd10d86267508e4cd730384f diff --git a/.git---/refs/tags/v3.3.25 b/.git---/refs/tags/v3.3.25 new file mode 100644 index 0000000..e07a6ea --- /dev/null +++ b/.git---/refs/tags/v3.3.25 @@ -0,0 +1 @@ +ddd6aaa8e1f00721bec2661058b57b9166f1a95d diff --git a/.git---/refs/tags/v3.3.26 b/.git---/refs/tags/v3.3.26 new file mode 100644 index 0000000..2490c88 --- /dev/null +++ b/.git---/refs/tags/v3.3.26 @@ -0,0 +1 @@ +38b399143e48ebcac29613fe615d65ecd7265a1b diff --git a/.git---/refs/tags/v3.3.27 b/.git---/refs/tags/v3.3.27 new file mode 100644 index 0000000..2aa6202 --- /dev/null +++ b/.git---/refs/tags/v3.3.27 @@ -0,0 +1 @@ +629e311d654726c86d628457296df8c745e66b5e diff --git a/.git---/refs/tags/v3.3.28 b/.git---/refs/tags/v3.3.28 new file mode 100644 index 0000000..82a115c --- /dev/null +++ b/.git---/refs/tags/v3.3.28 @@ -0,0 +1 @@ +9249de1e53db2b54a67339cf28c5bedea2b8d89c diff --git a/.git---/refs/tags/v3.3.29 b/.git---/refs/tags/v3.3.29 new file mode 100644 index 0000000..afa894a --- /dev/null +++ b/.git---/refs/tags/v3.3.29 @@ -0,0 +1 @@ +2349ac90c70f6e208d6f3e71c043bdbdcf333f40 diff --git a/.git---/refs/tags/v3.3.3 b/.git---/refs/tags/v3.3.3 new file mode 100644 index 0000000..b231b29 --- /dev/null +++ b/.git---/refs/tags/v3.3.3 @@ -0,0 +1 @@ +c7a670a31c67c2d5e28244b2b577e9a664241fa8 diff --git a/.git---/refs/tags/v3.3.30 b/.git---/refs/tags/v3.3.30 new file mode 100644 index 0000000..5b3ef5d --- /dev/null +++ b/.git---/refs/tags/v3.3.30 @@ -0,0 +1 @@ +115811e319d9509ecf1dbbe072d10830ca09f4ac diff --git a/.git---/refs/tags/v3.3.31 b/.git---/refs/tags/v3.3.31 new file mode 100644 index 0000000..099c7ab --- /dev/null +++ b/.git---/refs/tags/v3.3.31 @@ -0,0 +1 @@ +46a8af487db44cce47afce1c9dc9d82ad03cef2f diff --git a/.git---/refs/tags/v3.3.32 b/.git---/refs/tags/v3.3.32 new file mode 100644 index 0000000..1478c62 --- /dev/null +++ b/.git---/refs/tags/v3.3.32 @@ -0,0 +1 @@ +69752b6855512084fa45f168c63456ceba204414 diff --git a/.git---/refs/tags/v3.3.4 b/.git---/refs/tags/v3.3.4 new file mode 100644 index 0000000..2bbc233 --- /dev/null +++ b/.git---/refs/tags/v3.3.4 @@ -0,0 +1 @@ +292c638f3a74fac4b3f1a35560fa5b02cc15a111 diff --git a/.git---/refs/tags/v3.3.5 b/.git---/refs/tags/v3.3.5 new file mode 100644 index 0000000..499b726 --- /dev/null +++ b/.git---/refs/tags/v3.3.5 @@ -0,0 +1 @@ +0e2e11154581d80ab53ae9a7eb4c1b1ef1e24fdc diff --git a/.git---/refs/tags/v3.3.6 b/.git---/refs/tags/v3.3.6 new file mode 100644 index 0000000..692263e --- /dev/null +++ b/.git---/refs/tags/v3.3.6 @@ -0,0 +1 @@ +84260ee690fae17afa9e66902c9a5af0f41a3efe diff --git a/.git---/refs/tags/v3.3.7 b/.git---/refs/tags/v3.3.7 new file mode 100644 index 0000000..5af4180 --- /dev/null +++ b/.git---/refs/tags/v3.3.7 @@ -0,0 +1 @@ +3e1e8ea86edbdb6dbb13e8645b9bffab1062a148 diff --git a/.git---/refs/tags/v3.3.8 b/.git---/refs/tags/v3.3.8 new file mode 100644 index 0000000..bf84452 --- /dev/null +++ b/.git---/refs/tags/v3.3.8 @@ -0,0 +1 @@ +80907150357f0f550efdfee24f848851b542a08b diff --git a/.git---/refs/tags/v3.3.9 b/.git---/refs/tags/v3.3.9 new file mode 100644 index 0000000..b1db73a --- /dev/null +++ b/.git---/refs/tags/v3.3.9 @@ -0,0 +1 @@ +17402ce960ab7b57d7f809ed2741f725a50335e4 diff --git a/.git---/refs/tags/v3.4.0 b/.git---/refs/tags/v3.4.0 new file mode 100644 index 0000000..ef362c2 --- /dev/null +++ b/.git---/refs/tags/v3.4.0 @@ -0,0 +1 @@ +0154992e18f6a5a09199a151ee75661e78b9c12f diff --git a/.git---/refs/tags/v3.4.1 b/.git---/refs/tags/v3.4.1 new file mode 100644 index 0000000..b2bc1f7 --- /dev/null +++ b/.git---/refs/tags/v3.4.1 @@ -0,0 +1 @@ +c644e0144ee7a54c7a671b13ba3851a2036cc772 diff --git a/.git---/refs/tags/v3.4.10 b/.git---/refs/tags/v3.4.10 new file mode 100644 index 0000000..caef4ac --- /dev/null +++ b/.git---/refs/tags/v3.4.10 @@ -0,0 +1 @@ +b504a6e08783d181ba845cfed08d1b1e60d50615 diff --git a/.git---/refs/tags/v3.4.11 b/.git---/refs/tags/v3.4.11 new file mode 100644 index 0000000..59d87be --- /dev/null +++ b/.git---/refs/tags/v3.4.11 @@ -0,0 +1 @@ +c4e94203ff5ab49bcf781faaca509ea02cea6d2d diff --git a/.git---/refs/tags/v3.4.2 b/.git---/refs/tags/v3.4.2 new file mode 100644 index 0000000..e7a3e7c --- /dev/null +++ b/.git---/refs/tags/v3.4.2 @@ -0,0 +1 @@ +a67933e226d04da7538066f3376bed89ab43c22d diff --git a/.git---/refs/tags/v3.4.3 b/.git---/refs/tags/v3.4.3 new file mode 100644 index 0000000..892ecbc --- /dev/null +++ b/.git---/refs/tags/v3.4.3 @@ -0,0 +1 @@ +b77519fbfbd69c13cbdbf977106bff796f7a508e diff --git a/.git---/refs/tags/v3.4.4 b/.git---/refs/tags/v3.4.4 new file mode 100644 index 0000000..7ad05ff --- /dev/null +++ b/.git---/refs/tags/v3.4.4 @@ -0,0 +1 @@ +4a6cd534ac35d68e162cd4cbbfaf3b4e16763997 diff --git a/.git---/refs/tags/v3.4.5 b/.git---/refs/tags/v3.4.5 new file mode 100644 index 0000000..bfba431 --- /dev/null +++ b/.git---/refs/tags/v3.4.5 @@ -0,0 +1 @@ +1fe967d4b174a433270929decd24a61968124c6a diff --git a/.git---/refs/tags/v3.4.6 b/.git---/refs/tags/v3.4.6 new file mode 100644 index 0000000..41205cd --- /dev/null +++ b/.git---/refs/tags/v3.4.6 @@ -0,0 +1 @@ +c3242896baa3288ee33f3a8bd69cb37f2e22ac4f diff --git a/.git---/refs/tags/v3.4.7 b/.git---/refs/tags/v3.4.7 new file mode 100644 index 0000000..017f759 --- /dev/null +++ b/.git---/refs/tags/v3.4.7 @@ -0,0 +1 @@ +24d55204d9556f7b26a06fd61c5b80856a200355 diff --git a/.git---/refs/tags/v3.4.8 b/.git---/refs/tags/v3.4.8 new file mode 100644 index 0000000..7b7b18e --- /dev/null +++ b/.git---/refs/tags/v3.4.8 @@ -0,0 +1 @@ +fc16e99738737e821fb4996ae906a24968aab1f4 diff --git a/.git---/refs/tags/v3.4.9 b/.git---/refs/tags/v3.4.9 new file mode 100644 index 0000000..c588abf --- /dev/null +++ b/.git---/refs/tags/v3.4.9 @@ -0,0 +1 @@ +3b49bf086a0deb70c06a54ce5104359350c775fe diff --git a/.git---/refs/tags/v3.5.0 b/.git---/refs/tags/v3.5.0 new file mode 100644 index 0000000..a628172 --- /dev/null +++ b/.git---/refs/tags/v3.5.0 @@ -0,0 +1 @@ +1f74930b73fdd6eb425774a4010a1f762bbb5ee8 diff --git a/.git---/refs/tags/v3.6.0 b/.git---/refs/tags/v3.6.0 new file mode 100644 index 0000000..1562a2e --- /dev/null +++ b/.git---/refs/tags/v3.6.0 @@ -0,0 +1 @@ +0c66e5aac8c93f82fce7c66b1374657f216d3b23 diff --git a/.git---/refs/tags/v3.7.0 b/.git---/refs/tags/v3.7.0 new file mode 100644 index 0000000..e2616e5 --- /dev/null +++ b/.git---/refs/tags/v3.7.0 @@ -0,0 +1 @@ +bd7b8bd50ebb96bc3c3292be5e667fd2ff3995ed diff --git a/.git---/refs/tags/v3.8.0 b/.git---/refs/tags/v3.8.0 new file mode 100644 index 0000000..8b78c71 --- /dev/null +++ b/.git---/refs/tags/v3.8.0 @@ -0,0 +1 @@ +9cb0b6783383977606ab75f5e9764854594230e5 diff --git a/.git---/refs/tags/v3.9.0 b/.git---/refs/tags/v3.9.0 new file mode 100644 index 0000000..9999fd9 --- /dev/null +++ b/.git---/refs/tags/v3.9.0 @@ -0,0 +1 @@ +62aa45fb4793c2a27841f602db5a5d32e249ede1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..455ddc2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,79 @@ +# 环境变量文件(包含敏感信息,禁止提交) +.env +.env.* +*.env +*.env.local +.env.*.local + +# 测试文件(可能包含敏感配置) +# 仅忽略仓库根目录下的临时 test_*.py 脚本;tests/ 目录下的单元测试需要纳入版本控制。 +/test_*.py +!test_env.py + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 虚拟环境 +venv/ +ENV/ +env/ +.venv/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ +.cursorrules + +# 数据和日志 +data/ +logs/ +reports/ +*.db +*.sqlite +*.sqlite3 + +# 系统文件 +.DS_Store +Thumbs.db + +# 测试 +.pytest_cache/ +.coverage +htmlcov/ + +local/ + +run.sh + +verify_*.py + +# ignore claude files +.claude/ +CLAUDE.md + +# ignore static files +static/ +/apps/dsa-desktop/dist/ +/apps/dsa-desktop/node_modules/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..de45fd3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,121 @@ +# AGENTS.md + +本文件定义在本仓库中执行开发、Issue 分析、PR 审查时的统一行为准则。 + +## 1. 通用协作原则 + +- 语言与栈:Python 3.10+,遵循仓库现有架构与目录边界。 +- 配置约束:统一使用 `.env`(参见 `.env.example`)。 +- 代码质量:优先保证可运行、可回归验证、可追踪(日志/错误信息清晰)。 +- 风格约束: + - 行宽 120 + - `black` + `isort` + `flake8` + - 关键变更需至少做语法检查(`py_compile`)或对应测试验证。 + - 新增或修改的代码注释必须使用英文。 +- Git 约束: + - 未经明确确认,不执行 `git commit`。 + - commit message 不添加 `Co-Authored-By`。 + - 后续所有 commit message 必须使用英文。 + +## 2. Issue 分析原则 + +每个 Issue 必须先回答 3 个问题: + +1. 是否合理(Reasonable) +- 是否描述了真实影响(功能错误、数据错误、性能/稳定性问题、体验退化)。 +- 是否有可验证证据(日志、截图、复现步骤、版本信息)。 +- 是否与项目目标相关(股票分析、数据源、通知、API/WebUI、部署链路)。 + +2. 是否是 Issue(Valid Issue) +- 属于缺陷/功能缺失/回归/文档错误之一,而非纯咨询或环境误用。 +- 能定位到仓库责任边界;若是三方服务波动,也需判断是否需要仓库侧兜底。 +- 如果是使用问题,应转为文档改进或 FAQ,而不是代码缺陷。 + +3. 是否好解决(Solvability) +- 可否稳定复现。 +- 依赖是否可控(第三方 API、网络、权限、密钥)。 +- 变更范围与风险等级(低/中/高)。 +- 是否存在临时缓解方案(降级、兜底、开关、重试、回退策略)。 + +### Issue 结论模板 + +- 结论:`成立 / 部分成立 / 不成立` +- 分类:`bug / feature / docs / question / external` +- 优先级:`P0 / P1 / P2 / P3` +- 难度:`easy / medium / hard` +- 建议:`立即修复 / 排期修复 / 文档澄清 / 关闭` + +## 3. PR 分析原则 + +每个 PR 需按以下顺序审查: + +1. 必要性(Necessity) +- 是否解决明确问题,或提供明确业务价值。 +- 是否避免“为了改而改”的重构。 + +2. 关联性(Traceability) +- 是否关联对应 Issue(建议必须有:`Fixes #xxx` 或 `Refs #xxx`)。 +- 若无 Issue,PR 描述必须给出动机、场景与验收标准。 + +3. 类型判定(Type) +- 明确标注:`fix / feat / refactor / docs / chore / test`。 +- 对“fix/bug”类 PR:必须说明原问题、根因、修复点、回归风险。 + +4. 描述完整性(Description Completeness) +- 必须包含: + - 背景与问题 + - 变更范围(改了哪些模块) + - 验证方式与结果(命令、关键输出) + - 兼容性与破坏性变更说明(如有) + - 回滚方案(至少一句) + - 若为 issue 修复:在 PR description 中显式写明关闭语句(如 `Fixes #241` / `Closes #241`) + +5. 合入判定(Merge Readiness) +- 可直接合入(Ready to Merge)条件: + - 目标明确且必要 + - 有 Issue 或同等质量的问题描述 + - 变更与描述一致,无隐藏副作用 + - 关键验证已通过(语法/测试/关键链路) + - 无阻断性风险(安全、数据损坏、明显性能回退) +- 不可直接合入(Not Ready)条件: + - 描述不完整,无法确认动机和影响 + - 无验证证据 + - 引入明显风险且无回滚策略 + - 与仓库方向无关或重复实现 + +## 4. 交付与发布同步原则 + +- 功能开发、缺陷修复完成后,必须同步更新文档: + - `README.md`(用户可见能力、使用方式、配置项变化) + - `docs/CHANGELOG.md`(版本变更记录、影响范围、兼容性说明) +- 发布语义必须与改动规模匹配,在提交说明中添加对应 tag 标签: + - `#patch`:修复类、小改动 + - `#minor`:新增可用功能、向后兼容 + - `#major`:破坏性变更或重大架构调整 + - `#skip` / `#none`:明确不触发自动版本标签 +- 若改动用于解决已有 issue,PR description 必须声明关闭该 issue(`Fixes #xxx` / `Closes #xxx`),避免修复完成后 issue 悬挂。 + +## 5. 建议评审输出格式 + +### Issue 评审输出 + +- `是否合理`:是/否 + 理由 +- `是否是 issue`:是/否 + 理由 +- `是否好解决`:是/否 + 难点 +- `建议动作`:修复/排期/文档/关闭 + +### PR 评审输出 + +- `必要性`:通过/不通过 +- `是否有对应 issue`:有/无(编号) +- `PR 类型`:fix/feat/... +- `description 完整性`:完整/不完整(缺失项) +- `是否可直接合入`:可/不可 + 必改项 + +## 6. 快速检查命令(可选) + +```bash +./test.sh syntax +python -m py_compile main.py src/*.py data_provider/*.py +flake8 main.py src/ --max-line-length=120 +``` diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md new file mode 100644 index 0000000..c3f3598 --- /dev/null +++ b/DEPLOYMENT_GUIDE.md @@ -0,0 +1,545 @@ +# 📘 股票智能分析系统 - 部署运行指南 + +> 本文档涵盖项目的启动方式、部署方式及注意事项 + +--- + +## 📋 目录 + +1. [快速启动](#一快速启动) +2. [启动方式](#二启动方式) +3. [部署方式](#三部署方式) +4. [注意事项](#四注意事项) +5. [常见问题](#五常见问题) + +--- + +## 一、快速启动 + +### 1.1 环境要求 + +| 组件 | 最低要求 | 推荐版本 | +|------|----------|----------| +| Python | 3.10+ | 3.11.x | +| Node.js | 18+ | 20.x | +| Docker | 20.10+ | 最新版 | +| 内存 | 512MB | 1GB+ | +| 磁盘 | 1GB | 5GB+ | + +### 1.2 最小配置 + +复制配置文件并填写必要信息: + +```bash +# Windows +copy .env.example .env + +# Linux/Mac +cp .env.example .env +``` + +编辑 `.env` 文件,至少配置以下项: + +```env +# 1. 自选股列表(必填) +STOCK_LIST=600519,300750,002594 + +# 2. AI 模型(二选一) +# 方案 A: Gemini(免费) +GEMINI_API_KEY=your_gemini_key + +# 方案 B: OpenAI 兼容 API(如 DeepSeek) +OPENAI_API_KEY=your_key +OPENAI_BASE_URL=https://api.deepseek.com/v1 +OPENAI_MODEL=deepseek-chat + +# 3. 通知渠道(至少一个) +WECHAT_WEBHOOK_URL=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx +# 或 +EMAIL_SENDER=your_email@qq.com +EMAIL_PASSWORD=your_auth_code +``` + +--- + +## 二、启动方式 + +### 2.1 命令行模式 + +#### 基本命令 + +```bash +# 安装依赖 +pip install -r requirements.txt + +# 仅获取数据(测试模式,不消耗 AI 额度) +python main.py --dry-run + +# 完整运行(执行 AI 分析) +python main.py + +# 调试模式(输出详细日志) +python main.py --debug + +# 指定分析特定股票 +python main.py --stocks 600519,000001 + +# 不发送推送通知 +python main.py --no-notify + +# 单股推送模式(每分析完一只立即推送) +python main.py --single-notify + +# 仅运行大盘复盘 +python main.py --market-review +``` + +#### 启动 WebUI + +```bash +# 编译前端(首次需要) +cd apps/dsa-web +npm install +npm run build +cd ../.. + +# 启动 WebUI + 定时分析 +python main.py --webui + +# 仅启动 WebUI(不执行分析) +python main.py --webui-only + +# 或使用等效命令 +python main.py --serve +python main.py --serve-only +``` + +访问地址:http://127.0.0.1:8000 + +#### 定时任务模式 + +```bash +# 启用定时任务(每日自动执行) +python main.py --schedule + +# 自定义执行时间(默认 18:00) +SCHEDULE_TIME=09:30 python main.py --schedule +``` + +### 2.2 参数说明 + +| 参数 | 说明 | +|------|------| +| `--dry-run` | 仅获取数据,不进行 AI 分析 | +| `--debug` | 启用调试模式,输出详细日志 | +| `--stocks` | 指定股票代码,逗号分隔 | +| `--no-notify` | 不发送推送通知 | +| `--single-notify` | 单股推送模式 | +| `--schedule` | 启用定时任务 | +| `--market-review` | 仅运行大盘复盘 | +| `--webui` | 启动 WebUI + 定时分析 | +| `--webui-only` | 仅启动 WebUI | +| `--serve` | 启动 API 服务 | +| `--serve-only` | 仅启动 API 服务 | +| `--host` | 绑定地址(默认 127.0.0.1) | +| `--port` | 端口(默认 8000) | + +--- + +## 三、部署方式 + +### 3.1 GitHub Actions 部署(推荐) + +**优点**:零成本、免服务器、自动定时运行 + +#### 部署步骤 + +1. **Fork 仓库** + - 点击右上角 `Fork` 按钮 + +2. **配置 Secrets** + ``` + Settings → Secrets and variables → Actions → New repository secret + ``` + +3. **添加必要 Secrets** + + | Secret 名称 | 说明 | 必填 | + |------------|------|:----:| + | `STOCK_LIST` | 自选股代码 | ✅ | + | `GEMINI_API_KEY` 或 `OPENAI_API_KEY` | AI 模型 API Key | ✅ | + | `WECHAT_WEBHOOK_URL` / `EMAIL_SENDER` | 通知渠道 | ✅ | + +4. **启用 Actions** + ``` + Actions 标签 → I understand my workflows, go ahead and enable them + ``` + +5. **手动测试** + ``` + Actions → 每日股票分析 → Run workflow → Run workflow + ``` + +**默认定时**:工作日 18:00(北京时间)自动执行 + +### 3.2 Docker 部署 + +#### 快速启动 + +```bash +# 1. 克隆仓库 +git clone https://github.com/ZhuLinsen/daily_stock_analysis.git +cd daily_stock_analysis + +# 2. 配置环境变量 +cp .env.example .env +# 编辑 .env 填入配置 + +# 3. 启动容器 +# Web 服务模式(推荐,提供 API 与 WebUI) +docker-compose -f ./docker/docker-compose.yml up -d server + +# 定时任务模式 +docker-compose -f ./docker/docker-compose.yml up -d analyzer + +# 同时启动两种模式 +docker-compose -f ./docker/docker-compose.yml up -d +``` + +#### 访问服务 + +| 服务 | 地址 | +|------|------| +| WebUI | http://localhost:8000 | +| API | http://localhost:8000/api | + +#### 常用命令 + +```bash +# 查看日志 +docker-compose -f ./docker/docker-compose.yml logs -f server +docker-compose -f ./docker/docker-compose.yml logs -f analyzer + +# 停止服务 +docker-compose -f ./docker/docker-compose.yml down + +# 重启服务 +docker-compose -f ./docker/docker-compose.yml restart server + +# 更新镜像(拉取代码后) +docker-compose -f ./docker/docker-compose.yml up -d --build + +# 进入容器 +docker exec -it stock-server /bin/bash +``` + +#### Docker 运行模式 + +| 模式 | 命令 | 说明 | +|------|------|------| +| Web 服务 | `up -d server` | 提供 WebUI 和 API | +| 定时分析 | `up -d analyzer` | 每日自动执行分析 | +| 双模式 | `up -d` | 同时启动两种模式 | + +### 3.3 本地服务器部署 + +#### Linux/Mac 系统 + +```bash +# 1. 克隆项目 +git clone https://github.com/ZhuLinsen/daily_stock_analysis.git +cd daily_stock_analysis + +# 2. 创建虚拟环境 +python -m venv venv +source venv/bin/activate + +# 3. 安装依赖 +pip install -r requirements.txt + +# 4. 编译前端 +cd apps/dsa-web +npm install +npm run build +cd ../.. + +# 5. 配置环境 +cp .env.example .env +vim .env + +# 6. 启动服务 +# 方式 A: 直接运行 +python main.py --webui + +# 方式 B: 使用 systemd(推荐生产环境) +sudo nano /etc/systemd/system/stock-analysis.service +``` + +**systemd 服务配置示例**: + +```ini +[Unit] +Description=Stock Analysis Service +After=network.target + +[Service] +Type=simple +User=ubuntu +WorkingDirectory=/home/ubuntu/daily_stock_analysis +Environment=PYTHONUNBUFFERED=1 +ExecStart=/home/ubuntu/daily_stock_analysis/venv/bin/python main.py --webui +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +启动服务: +```bash +sudo systemctl daemon-reload +sudo systemctl enable stock-analysis +sudo systemctl start stock-analysis +sudo systemctl status stock-analysis +``` + +#### Windows 系统 + +```powershell +# 1. 克隆项目 +git clone https://github.com/ZhuLinsen/daily_stock_analysis.git +cd daily_stock_analysis + +# 2. 安装依赖 +pip install -r requirements.txt + +# 3. 编译前端 +cd apps\dsa-web +npm install +npm run build +cd ..\.. + +# 4. 配置环境 +copy .env.example .env +# 编辑 .env + +# 5. 启动 +python main.py --webui +``` + +**使用 NSSM 注册 Windows 服务**(生产环境): + +```powershell +# 下载 NSSM: https://nssm.cc/download +nssm install stock-analysis +# 设置 Path: python +# 设置 Arguments: main.py --webui +# 设置 Working directory: D:\daily_stock_analysis + +nssm start stock-analysis +``` + +### 3.4 部署方式对比 + +| 部署方式 | 复杂度 | 成本 | 适用场景 | 稳定性 | +|----------|--------|------|----------|--------| +| GitHub Actions | ⭐ 低 | 免费 | 个人用户、轻度使用 | ⭐⭐⭐ | +| Docker | ⭐⭐ 中 | 低 | 有服务器的用户 | ⭐⭐⭐⭐ | +| 本地服务器 | ⭐⭐⭐ 高 | 中 | 企业用户、高频使用 | ⭐⭐⭐⭐⭐ | +| 云服务器 | ⭐⭐ 中 | 中 | 需要 24h 在线服务 | ⭐⭐⭐⭐⭐ | + +--- + +## 四、注意事项 + +### 4.1 安全配置 + +#### API Key 保护 + +```bash +# ❌ 不要这样做 +# 在代码中硬编码 API Key +# 提交 .env 到 Git 仓库 + +# ✅ 正确做法 +# 使用环境变量 +# 将 .env 加入 .gitignore +# GitHub Actions 使用 Secrets +``` + +#### 最小权限原则 + +| 服务 | 建议权限 | +|------|----------| +| AI API Key | 仅文本生成权限 | +| 邮件授权码 | 仅 SMTP 发送权限 | +| Tushare Token | 仅行情数据权限 | + +### 4.2 性能优化 + +#### 并发控制 + +```env +# .env 中配置 +MAX_WORKERS=3 # 根据机器性能调整,建议 2-5 +``` + +#### API 限流处理 + +```env +# 个股分析和大盘分析之间的延迟(秒) +ANALYSIS_DELAY=10 + +# Gemini 请求延迟 +GEMINI_REQUEST_DELAY=30 +``` + +#### 内存优化(Docker) + +```yaml +# docker-compose.yml +deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M +``` + +### 4.3 数据备份 + +```bash +# 数据库备份(SQLite) +cp data/stock_analysis.db backup/stock_analysis_$(date +%Y%m%d).db + +# 日志归档 +tar -czf logs_backup_$(date +%Y%m%d).tar.gz logs/ + +# Docker 数据卷备份 +docker run --rm -v daily_stock_analysis_data:/data -v $(pwd):/backup alpine tar czf /backup/data_backup.tar.gz -C /data . +``` + +### 4.4 监控与日志 + +#### 日志位置 + +| 部署方式 | 日志路径 | +|----------|----------| +| 本地运行 | `./logs/` | +| Docker | `/app/logs/` 或挂载的宿主机目录 | +| GitHub Actions | Actions 运行日志 | + +#### 关键日志文件 + +``` +logs/ +├── stock_analysis_YYYYMMDD.log # 常规日志 +├── stock_analysis_debug_YYYYMMDD.log # 调试日志 +└── notifications_YYYYMMDD.log # 推送日志 +``` + +#### 健康检查 + +```bash +# API 健康检查 +curl http://localhost:8000/api/health + +# Docker 健康检查(自动) +docker ps # 查看 STATUS 列 +``` + +### 4.5 网络与代理 + +```env +# 国内用户可能需要代理访问 Gemini/OpenAI +USE_PROXY=true +PROXY_HOST=127.0.0.1 +PROXY_PORT=10809 + +# Docker 代理配置 +# docker-compose.yml 中 environment 部分 +- http_proxy=http://host.docker.internal:10809 +- https_proxy=http://host.docker.internal:10809 +``` + +### 4.6 时区设置 + +```env +# .env 中设置 +TZ=Asia/Shanghai + +# Docker 中已默认设置上海时区 +``` + +--- + +## 五、常见问题 + +### Q1: Windows 下中文显示乱码? + +```powershell +# 设置 UTF-8 编码 +chcp 65001 + +# 或在运行前设置环境变量 +$env:PYTHONIOENCODING="utf-8" +python main.py +``` + +### Q2: 前端页面 404? + +```bash +# 需要编译前端 +cd apps/dsa-web +npm install +npm run build +``` + +### Q3: Docker 启动失败? + +```bash +# 检查端口占用 +netstat -tlnp | grep 8000 + +# 检查环境变量 +cat .env | grep -E "^(STOCK_LIST|OPENAI|GEMINI)" + +# 查看详细日志 +docker-compose logs +``` + +### Q4: API 额度不足? + +- **Gemini**: 免费版有额度限制,建议申请多个 Key 轮换使用 +- **DeepSeek**: 充值或降低分析频率 +- **本地模型**: 使用 Ollama 部署本地大模型 + +### Q5: 推送收不到? + +1. 检查 Webhook URL 是否正确 +2. 检查网络连通性:`curl -v WEBHOOK_URL` +3. 查看通知日志:`logs/notifications_*.log` +4. 确认消息长度未超限(企业微信 4096 字节) + +### Q6: 数据获取失败? + +```bash +# 检查数据源状态 +python -c "from data_provider.base import DataFetcherManager; m = DataFetcherManager(); print(m.list_fetchers())" + +# 单个数据源测试 +python -c "from data_provider.tushare_fetcher import TushareFetcher; f = TushareFetcher(); print(f.test_connection())" +``` + +--- + +## 六、参考链接 + +- [完整配置指南](docs/full-guide.md) +- [常见问题](docs/FAQ.md) +- [更新日志](docs/CHANGELOG.md) +- [项目主页](https://github.com/ZhuLinsen/daily_stock_analysis) + +--- + +**免责声明**: 本项目仅供学习和研究使用,不构成任何投资建议。股市有风险,投资需谨慎。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3f8e14a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 ZhuLinsen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..54501f2 --- /dev/null +++ b/README.md @@ -0,0 +1,283 @@ +
+ +# 📈 股票智能分析系统 + +[![GitHub stars](https://img.shields.io/github/stars/ZhuLinsen/daily_stock_analysis?style=social)](https://github.com/ZhuLinsen/daily_stock_analysis/stargazers) +[![CI](https://github.com/ZhuLinsen/daily_stock_analysis/actions/workflows/ci.yml/badge.svg)](https://github.com/ZhuLinsen/daily_stock_analysis/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/) +[![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-Ready-2088FF?logo=github-actions&logoColor=white)](https://github.com/features/actions) +[![Docker](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker&logoColor=white)](https://hub.docker.com/) + +> 🤖 基于 AI 大模型的 A股/港股/美股自选股智能分析系统,每日自动分析并推送「决策仪表盘」到企业微信/飞书/Telegram/邮箱 + +[**功能特性**](#-功能特性) · [**快速开始**](#-快速开始) · [**推送效果**](#-推送效果) · [**完整指南**](docs/full-guide.md) · [**常见问题**](docs/FAQ.md) · [**更新日志**](docs/CHANGELOG.md) + +简体中文 | [English](docs/README_EN.md) | [繁體中文](docs/README_CHT.md) + +
+ +## 💖 赞助商 (Sponsors) + +
+ + +## ✨ 功能特性 + +| 模块 | 功能 | 说明 | +|------|------|------| +| AI | 决策仪表盘 | 一句话核心结论 + 精确买卖点位 + 操作检查清单 | +| 分析 | 多维度分析 | 技术面 + 筹码分布 + 舆情情报 + 实时行情 | +| 市场 | 全球市场 | 支持 A股、港股、美股 | +| 复盘 | 大盘复盘 | 每日市场概览、板块涨跌、北向资金 | +| 回测 | AI 回测验证 | 自动评估历史分析准确率,方向胜率、止盈止损命中率 | +| 推送 | 多渠道通知 | 企业微信、飞书、Telegram、钉钉、邮件、Pushover | +| 自动化 | 定时运行 | GitHub Actions 定时执行,无需服务器 | + +### 技术栈与数据来源 + +| 类型 | 支持 | +|------|------| +| AI 模型 | Gemini(免费)、OpenAI 兼容、DeepSeek、通义千问、Claude、Ollama | +| 行情数据 | AkShare、Tushare、Pytdx、Baostock、YFinance | +| 新闻搜索 | Tavily、SerpAPI、Bocha、Brave | + +### 内置交易纪律 + +| 规则 | 说明 | +|------|------| +| 严禁追高 | 乖离率 > 5% 自动提示风险 | +| 趋势交易 | MA5 > MA10 > MA20 多头排列 | +| 精确点位 | 买入价、止损价、目标价 | +| 检查清单 | 每项条件以「满足 / 注意 / 不满足」标记 | + +## 🚀 快速开始 + +### 方式一:GitHub Actions(推荐) + +> 5 分钟完成部署,零成本,无需服务器。 + + +#### 1. Fork 本仓库 + +点击右上角 `Fork` 按钮(顺便点个 Star⭐ 支持一下) + +#### 2. 配置 Secrets + +`Settings` → `Secrets and variables` → `Actions` → `New repository secret` + +**AI 模型配置(二选一)** + +| Secret 名称 | 说明 | 必填 | +|------------|------|:----:| +| `GEMINI_API_KEY` | [Google AI Studio](https://aistudio.google.com/) 获取免费 Key | ✅* | +| `OPENAI_API_KEY` | OpenAI 兼容 API Key(支持 DeepSeek、通义千问等) | 可选 | +| `OPENAI_BASE_URL` | OpenAI 兼容 API 地址(如 `https://api.deepseek.com/v1`) | 可选 | +| `OPENAI_MODEL` | 模型名称(如 `deepseek-chat`) | 可选 | + +> 注:`GEMINI_API_KEY` 和 `OPENAI_API_KEY` 至少配置一个 + +
+通知渠道配置(点击展开,至少配置一个) + + +| Secret 名称 | 说明 | 必填 | +|------------|------|:----:| +| `WECHAT_WEBHOOK_URL` | 企业微信 Webhook URL | 可选 | +| `FEISHU_WEBHOOK_URL` | 飞书 Webhook URL | 可选 | +| `TELEGRAM_BOT_TOKEN` | Telegram Bot Token(@BotFather 获取) | 可选 | +| `TELEGRAM_CHAT_ID` | Telegram Chat ID | 可选 | +| `TELEGRAM_MESSAGE_THREAD_ID` | Telegram Topic ID (用于发送到子话题) | 可选 | +| `EMAIL_SENDER` | 发件人邮箱(如 `xxx@qq.com`) | 可选 | +| `EMAIL_PASSWORD` | 邮箱授权码(非登录密码) | 可选 | +| `EMAIL_RECEIVERS` | 收件人邮箱(多个用逗号分隔,留空则发给自己) | 可选 | +| `EMAIL_SENDER_NAME` | 邮件发件人显示名称(默认:daily_stock_analysis股票分析助手) | 可选 | +| `STOCK_GROUP_N` / `EMAIL_GROUP_N` | 股票分组发往不同邮箱(如 `STOCK_GROUP_1=600519,300750` `EMAIL_GROUP_1=user1@example.com`) | 可选 | +| `PUSHPLUS_TOKEN` | PushPlus Token([获取地址](https://www.pushplus.plus),国内推送服务) | 可选 | +| `SERVERCHAN3_SENDKEY` | Server酱³ Sendkey([获取地址](https://sc3.ft07.com/),手机APP推送服务) | 可选 | +| `CUSTOM_WEBHOOK_URLS` | 自定义 Webhook(支持钉钉等,多个用逗号分隔) | 可选 | +| `CUSTOM_WEBHOOK_BEARER_TOKEN` | 自定义 Webhook 的 Bearer Token(用于需要认证的 Webhook) | 可选 | +| `SINGLE_STOCK_NOTIFY` | 单股推送模式:设为 `true` 则每分析完一只股票立即推送 | 可选 | +| `REPORT_TYPE` | 报告类型:`simple`(精简) 或 `full`(完整),Docker环境推荐设为 `full` | 可选 | +| `ANALYSIS_DELAY` | 个股分析和大盘分析之间的延迟(秒),避免API限流,如 `10` | 可选 | + +> 至少配置一个渠道,配置多个则同时推送。更多配置请参考 [完整指南](docs/full-guide.md) + +
+ +**其他配置** + +| Secret 名称 | 说明 | 必填 | +|------------|------|:----:| +| `STOCK_LIST` | 自选股代码,如 `600519,hk00700,AAPL,TSLA` | ✅ | +| `TAVILY_API_KEYS` | [Tavily](https://tavily.com/) 搜索 API(新闻搜索) | 推荐 | +| `SERPAPI_API_KEYS` | [SerpAPI](https://serpapi.com/baidu-search-api?utm_source=github_daily_stock_analysis) 全渠道搜索 | 可选 | +| `BOCHA_API_KEYS` | [博查搜索](https://open.bocha.cn/) Web Search API(中文搜索优化,支持AI摘要,多个key用逗号分隔) | 可选 | +| `BRAVE_API_KEYS` | [Brave Search](https://brave.com/search/api/) API(隐私优先,美股优化,多个key用逗号分隔) | 可选 | +| `TUSHARE_TOKEN` | [Tushare Pro](https://tushare.pro/weborder/#/login?reg=834638 ) Token | 可选 | +| `WECHAT_MSG_TYPE` | 企微消息类型,默认 markdown,支持配置 text 类型,发送纯 markdown 文本 | 可选 | + +#### 3. 启用 Actions + +`Actions` 标签 → `I understand my workflows, go ahead and enable them` + +#### 4. 手动测试 + +`Actions` → `每日股票分析` → `Run workflow` → `Run workflow` + +#### 完成 + +默认每个**工作日 18:00(北京时间)**自动执行,也可手动触发 + +### 方式二:本地运行 / Docker 部署 + +```bash +# 克隆项目 +git clone https://github.com/ZhuLinsen/daily_stock_analysis.git && cd daily_stock_analysis + +# 安装依赖 +pip install -r requirements.txt + +# 配置环境变量 +cp .env.example .env && vim .env + +# 运行分析 +python main.py +``` + +> Docker 部署、定时任务配置请参考 [完整指南](docs/full-guide.md) + +## 📱 推送效果 + +### 决策仪表盘 +``` +🎯 2026-02-08 决策仪表盘 +共分析3只股票 | 🟢买入:0 🟡观望:2 🔴卖出:1 + +📊 分析结果摘要 +⚪ 中钨高新(000657): 观望 | 评分 65 | 看多 +⚪ 永鼎股份(600105): 观望 | 评分 48 | 震荡 +🟡 新莱应材(300260): 卖出 | 评分 35 | 看空 + +⚪ 中钨高新 (000657) +📰 重要信息速览 +💭 舆情情绪: 市场关注其AI属性与业绩高增长,情绪偏积极,但需消化短期获利盘和主力流出压力。 +📊 业绩预期: 基于舆情信息,公司2025年前三季度业绩同比大幅增长,基本面强劲,为股价提供支撑。 + +🚨 风险警报: + +风险点1:2月5日主力资金大幅净卖出3.63亿元,需警惕短期抛压。 +风险点2:筹码集中度高达35.15%,表明筹码分散,拉升阻力可能较大。 +风险点3:舆情中提及公司历史违规记录及重组相关风险提示,需保持关注。 +✨ 利好催化: + +利好1:公司被市场定位为AI服务器HDI核心供应商,受益于AI产业发展。 +利好2:2025年前三季度扣非净利润同比暴涨407.52%,业绩表现强劲。 +📢 最新动态: 【最新消息】舆情显示公司是AI PCB微钻领域龙头,深度绑定全球头部PCB/载板厂。2月5日主力资金净卖出3.63亿元,需关注后续资金流向。 + +--- +生成时间: 18:00 +``` + +### 大盘复盘 +``` +🎯 2026-01-10 大盘复盘 + +📊 主要指数 +- 上证指数: 3250.12 (🟢+0.85%) +- 深证成指: 10521.36 (🟢+1.02%) +- 创业板指: 2156.78 (🟢+1.35%) + +📈 市场概况 +上涨: 3920 | 下跌: 1349 | 涨停: 155 | 跌停: 3 + +🔥 板块表现 +领涨: 互联网服务、文化传媒、小金属 +领跌: 保险、航空机场、光伏设备 +``` +## ⚙️ 配置说明 + +> 📖 完整环境变量、定时任务配置请参考 [完整配置指南](docs/full-guide.md) + + +## 🖥️ Web 界面 + +![img.png](sources/fastapi_server.png) + +包含完整的配置管理、任务监控和手动分析功能。 + +### 启动方式 + +1. **编译前端** (首次运行需要) + ```bash + cd ./apps/dsa-web + npm install && npm run build + cd ../.. + ``` + +2. **启动服务** + ```bash + python main.py --webui # 启动 Web 界面 + 执行定时分析 + python main.py --webui-only # 仅启动 Web 界面 + ``` + +访问 `http://127.0.0.1:8000` 即可使用。 + +> 也可以使用 `python main.py --serve` (等效命令) + +## 🗺️ Roadmap + +查看已支持的功能和未来规划:[更新日志](docs/CHANGELOG.md) + +> 有建议?欢迎 [提交 Issue](https://github.com/ZhuLinsen/daily_stock_analysis/issues) + + +--- + +## ☕ 支持项目 + +如果本项目对你有帮助,欢迎支持项目的持续维护与迭代,感谢支持 🙏 +赞赏可备注联系方式,祝股市长虹 + +| 支付宝 (Alipay) | 微信支付 (WeChat) | Ko-fi | +| :---: | :---: | :---: | +| Alipay | WeChat Pay | Ko-fi | + +--- + +## 🤝 贡献 + +欢迎提交 Issue 和 Pull Request! + +详见 [贡献指南](docs/CONTRIBUTING.md) + +## 📄 License +[MIT License](LICENSE) © 2026 ZhuLinsen + +如果你在项目中使用或基于本项目进行二次开发, +非常欢迎在 README 或文档中注明来源并附上本仓库链接。 +这将有助于项目的持续维护和社区发展。 + +## 📬 联系与合作 +- GitHub Issues:[提交 Issue](https://github.com/ZhuLinsen/daily_stock_analysis/issues) + +## ⭐ Star History +**如果觉得有用,请给个 ⭐ Star 支持一下!** + + + + + + Star History Chart + + + +## ⚠️ 免责声明 + +本项目仅供学习和研究使用,不构成任何投资建议。股市有风险,投资需谨慎。作者不对使用本项目产生的任何损失负责。 + +--- diff --git a/analyzer_service.py b/analyzer_service.py new file mode 100644 index 0000000..e361e1f --- /dev/null +++ b/analyzer_service.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +""" +=================================== +A股自选股智能分析系统 - 分析服务层 +=================================== + +职责: +1. 封装核心分析逻辑,支持多调用方(CLI、WebUI、Bot) +2. 提供清晰的API接口,不依赖于命令行参数 +3. 支持依赖注入,便于测试和扩展 +4. 统一管理分析流程和配置 +""" + +import uuid +from typing import List, Optional + +from src.analyzer import AnalysisResult +from src.config import get_config, Config +from src.notification import NotificationService +from src.enums import ReportType +from src.core.pipeline import StockAnalysisPipeline +from src.core.market_review import run_market_review + + + +def analyze_stock( + stock_code: str, + config: Config = None, + full_report: bool = False, + notifier: Optional[NotificationService] = None +) -> Optional[AnalysisResult]: + """ + 分析单只股票 + + Args: + stock_code: 股票代码 + config: 配置对象(可选,默认使用单例) + full_report: 是否生成完整报告 + notifier: 通知服务(可选) + + Returns: + 分析结果对象 + """ + if config is None: + config = get_config() + + # 创建分析流水线 + pipeline = StockAnalysisPipeline( + config=config, + query_id=uuid.uuid4().hex, + query_source="cli" + ) + + # 使用通知服务(如果提供) + if notifier: + pipeline.notifier = notifier + + # 根据full_report参数设置报告类型 + report_type = ReportType.FULL if full_report else ReportType.SIMPLE + + # 运行单只股票分析 + result = pipeline.process_single_stock( + code=stock_code, + skip_analysis=False, + single_stock_notify=notifier is not None, + report_type=report_type + ) + + return result + +def analyze_stocks( + stock_codes: List[str], + config: Config = None, + full_report: bool = False, + notifier: Optional[NotificationService] = None +) -> List[AnalysisResult]: + """ + 分析多只股票 + + Args: + stock_codes: 股票代码列表 + config: 配置对象(可选,默认使用单例) + full_report: 是否生成完整报告 + notifier: 通知服务(可选) + + Returns: + 分析结果列表 + """ + if config is None: + config = get_config() + + results = [] + for stock_code in stock_codes: + result = analyze_stock(stock_code, config, full_report, notifier) + if result: + results.append(result) + + return results + +def perform_market_review( + config: Config = None, + notifier: Optional[NotificationService] = None +) -> Optional[str]: + """ + 执行大盘复盘 + + Args: + config: 配置对象(可选,默认使用单例) + notifier: 通知服务(可选) + + Returns: + 复盘报告内容 + """ + if config is None: + config = get_config() + + # 创建分析流水线以获取analyzer和search_service + pipeline = StockAnalysisPipeline( + config=config, + query_id=uuid.uuid4().hex, + query_source="cli" + ) + + # 使用提供的通知服务或创建新的 + review_notifier = notifier or pipeline.notifier + + # 调用大盘复盘函数 + return run_market_review( + notifier=review_notifier, + analyzer=pipeline.analyzer, + search_service=pipeline.search_service + ) + + diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..5cbed5b --- /dev/null +++ b/api/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +""" +=================================== +API 模块初始化 +=================================== + +职责: +1. 导出 API 模块的公共接口 +2. 统一版本管理 +""" + +__version__ = "1.0.0" diff --git a/api/app.py b/api/app.py new file mode 100644 index 0000000..7c8e036 --- /dev/null +++ b/api/app.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +""" +=================================== +FastAPI 应用工厂模块 +=================================== + +职责: +1. 创建和配置 FastAPI 应用实例 +2. 配置 CORS 中间件 +3. 注册路由和异常处理器 +4. 托管前端静态文件(生产模式) + +使用方式: + from api.app import create_app + app = create_app() +""" + +import os +from contextlib import asynccontextmanager +from datetime import datetime +from pathlib import Path +from typing import Optional + +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse + +from api.v1 import api_v1_router +from api.middlewares.error_handler import add_error_handlers +from api.v1.schemas.common import RootResponse, HealthResponse +from src.services.system_config_service import SystemConfigService + + +@asynccontextmanager +async def app_lifespan(app: FastAPI): + """Initialize and release shared services for the app lifecycle.""" + app.state.system_config_service = SystemConfigService() + try: + yield + finally: + if hasattr(app.state, "system_config_service"): + delattr(app.state, "system_config_service") + + +def create_app(static_dir: Optional[Path] = None) -> FastAPI: + """ + 创建并配置 FastAPI 应用实例 + + Args: + static_dir: 静态文件目录路径(可选,默认为项目根目录下的 static) + + Returns: + 配置完成的 FastAPI 应用实例 + """ + # 默认静态文件目录 + if static_dir is None: + static_dir = Path(__file__).parent.parent / "static" + + # 创建 FastAPI 实例 + app = FastAPI( + title="Daily Stock Analysis API", + description=( + "A股/港股/美股自选股智能分析系统 API\n\n" + "## 功能模块\n" + "- 股票分析:触发 AI 智能分析\n" + "- 历史记录:查询历史分析报告\n" + "- 股票数据:获取行情数据\n\n" + "## 认证方式\n" + "当前版本暂无认证要求" + ), + version="1.0.0", + lifespan=app_lifespan, + ) + + # ============================================================ + # CORS 配置 + # ============================================================ + + allowed_origins = [ + "http://localhost:5173", + "http://127.0.0.1:5173", + "http://localhost:3000", + "http://127.0.0.1:3000", + ] + + # 从环境变量添加额外的允许来源 + extra_origins = os.environ.get("CORS_ORIGINS", "") + if extra_origins: + allowed_origins.extend([o.strip() for o in extra_origins.split(",") if o.strip()]) + + # 允许所有来源(开发/演示用) + if os.environ.get("CORS_ALLOW_ALL", "").lower() == "true": + allowed_origins = ["*"] + + app.add_middleware( + CORSMiddleware, + allow_origins=allowed_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + # ============================================================ + # 注册路由 + # ============================================================ + + app.include_router(api_v1_router) + add_error_handlers(app) + + # ============================================================ + # 根路由和健康检查 + # ============================================================ + + has_frontend = static_dir.exists() and (static_dir / "index.html").exists() + + if has_frontend: + @app.get("/", include_in_schema=False) + async def root(): + """根路由 - 返回前端页面""" + return FileResponse(static_dir / "index.html") + else: + @app.get( + "/", + response_model=RootResponse, + tags=["Health"], + summary="API 根路由", + description="返回 API 运行状态信息" + ) + async def root() -> RootResponse: + """根路由 - API 状态信息""" + return RootResponse( + message="Daily Stock Analysis API is running", + version="1.0.0" + ) + + @app.get( + "/api/health", + response_model=HealthResponse, + tags=["Health"], + summary="健康检查", + description="用于负载均衡器或监控系统检查服务状态" + ) + async def health_check() -> HealthResponse: + """健康检查接口""" + return HealthResponse( + status="ok", + timestamp=datetime.now().isoformat() + ) + + # ============================================================ + # 静态文件托管(前端 SPA) + # ============================================================ + + if has_frontend: + # 挂载静态资源目录 + assets_dir = static_dir / "assets" + if assets_dir.exists(): + app.mount("/assets", StaticFiles(directory=assets_dir), name="assets") + + # SPA 路由回退 + @app.get("/{full_path:path}", include_in_schema=False) + async def serve_spa(request: Request, full_path: str): + """SPA 路由回退 - 非 API 路由返回 index.html""" + if full_path.startswith("api/"): + return None + + file_path = static_dir / full_path + if file_path.exists() and file_path.is_file(): + return FileResponse(file_path) + + return FileResponse(static_dir / "index.html") + + return app + + +# 默认应用实例(供 uvicorn 直接使用) +app = create_app() diff --git a/api/deps.py b/api/deps.py new file mode 100644 index 0000000..527bc26 --- /dev/null +++ b/api/deps.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" +=================================== +API 依赖注入模块 +=================================== + +职责: +1. 提供数据库 Session 依赖 +2. 提供配置依赖 +3. 提供服务层依赖 +""" + +from typing import Generator + +from fastapi import Request +from sqlalchemy.orm import Session + +from src.storage import DatabaseManager +from src.config import get_config, Config +from src.services.system_config_service import SystemConfigService + + +def get_db() -> Generator[Session, None, None]: + """ + 获取数据库 Session 依赖 + + 使用 FastAPI 依赖注入机制,确保请求结束后自动关闭 Session + + Yields: + Session: SQLAlchemy Session 对象 + + Example: + @router.get("/items") + async def get_items(db: Session = Depends(get_db)): + ... + """ + db_manager = DatabaseManager.get_instance() + session = db_manager.get_session() + try: + yield session + finally: + session.close() + + +def get_config_dep() -> Config: + """ + 获取配置依赖 + + Returns: + Config: 配置单例对象 + """ + return get_config() + + +def get_database_manager() -> DatabaseManager: + """ + 获取数据库管理器依赖 + + Returns: + DatabaseManager: 数据库管理器单例对象 + """ + return DatabaseManager.get_instance() + + +def get_system_config_service(request: Request) -> SystemConfigService: + """Get app-lifecycle shared SystemConfigService instance.""" + service = getattr(request.app.state, "system_config_service", None) + if service is None: + service = SystemConfigService() + request.app.state.system_config_service = service + return service diff --git a/api/middlewares/__init__.py b/api/middlewares/__init__.py new file mode 100644 index 0000000..0f9f4b6 --- /dev/null +++ b/api/middlewares/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +""" +=================================== +API 中间件模块初始化 +=================================== + +职责: +1. 导出所有中间件 +""" + +from api.middlewares.error_handler import ErrorHandlerMiddleware + +__all__ = ["ErrorHandlerMiddleware"] diff --git a/api/middlewares/error_handler.py b/api/middlewares/error_handler.py new file mode 100644 index 0000000..a88b769 --- /dev/null +++ b/api/middlewares/error_handler.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +""" +=================================== +全局异常处理中间件 +=================================== + +职责: +1. 捕获未处理的异常 +2. 统一错误响应格式 +3. 记录错误日志 +""" + +import logging +import traceback +from typing import Callable + +from fastapi import Request, Response +from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware + +logger = logging.getLogger(__name__) + + +class ErrorHandlerMiddleware(BaseHTTPMiddleware): + """ + 全局异常处理中间件 + + 捕获所有未处理的异常,返回统一格式的错误响应 + """ + + async def dispatch( + self, + request: Request, + call_next: Callable + ) -> Response: + """ + 处理请求,捕获异常 + + Args: + request: 请求对象 + call_next: 下一个处理器 + + Returns: + Response: 响应对象 + """ + try: + response = await call_next(request) + return response + + except Exception as e: + # 记录错误日志 + logger.error( + f"未处理的异常: {e}\n" + f"请求路径: {request.url.path}\n" + f"请求方法: {request.method}\n" + f"堆栈: {traceback.format_exc()}" + ) + + # 返回统一格式的错误响应 + return JSONResponse( + status_code=500, + content={ + "error": "internal_error", + "message": "服务器内部错误,请稍后重试", + "detail": str(e) if logger.isEnabledFor(logging.DEBUG) else None + } + ) + + +def add_error_handlers(app) -> None: + """ + 添加全局异常处理器 + + 为 FastAPI 应用添加各类异常的处理器 + + Args: + app: FastAPI 应用实例 + """ + from fastapi import HTTPException + from fastapi.exceptions import RequestValidationError + + @app.exception_handler(HTTPException) + async def http_exception_handler(request: Request, exc: HTTPException): + """处理 HTTP 异常""" + # 如果 detail 已经是 ErrorResponse 格式的 dict,直接使用 + if isinstance(exc.detail, dict) and "error" in exc.detail and "message" in exc.detail: + return JSONResponse( + status_code=exc.status_code, + content=exc.detail + ) + # 否则将 detail 包装成 ErrorResponse 格式 + return JSONResponse( + status_code=exc.status_code, + content={ + "error": "http_error", + "message": str(exc.detail) if exc.detail else "HTTP Error", + "detail": None + } + ) + + @app.exception_handler(RequestValidationError) + async def validation_exception_handler(request: Request, exc: RequestValidationError): + """处理请求验证异常""" + return JSONResponse( + status_code=422, + content={ + "error": "validation_error", + "message": "请求参数验证失败", + "detail": exc.errors() + } + ) + + @app.exception_handler(Exception) + async def general_exception_handler(request: Request, exc: Exception): + """处理通用异常""" + logger.error( + f"未处理的异常: {exc}\n" + f"请求路径: {request.url.path}\n" + f"堆栈: {traceback.format_exc()}" + ) + return JSONResponse( + status_code=500, + content={ + "error": "internal_error", + "message": "服务器内部错误", + "detail": None + } + ) diff --git a/api/v1/__init__.py b/api/v1/__init__.py new file mode 100644 index 0000000..2c308a5 --- /dev/null +++ b/api/v1/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +""" +=================================== +API v1 模块初始化 +=================================== + +职责: +1. 导出 v1 版本 API 的路由 +""" + +from api.v1.router import router as api_v1_router + +__all__ = ["api_v1_router"] diff --git a/api/v1/endpoints/__init__.py b/api/v1/endpoints/__init__.py new file mode 100644 index 0000000..4a2ca06 --- /dev/null +++ b/api/v1/endpoints/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +""" +=================================== +API v1 Endpoints 模块初始化 +=================================== + +职责: +1. 导出所有 endpoint 路由模块 +""" + +from api.v1.endpoints import health, analysis, history, stocks, backtest, system_config + +__all__ = ["health", "analysis", "history", "stocks", "backtest", "system_config"] diff --git a/api/v1/endpoints/analysis.py b/api/v1/endpoints/analysis.py new file mode 100644 index 0000000..8be5354 --- /dev/null +++ b/api/v1/endpoints/analysis.py @@ -0,0 +1,561 @@ +# -*- coding: utf-8 -*- +""" +=================================== +股票分析接口 +=================================== + +职责: +1. 提供 POST /api/v1/analysis/analyze 触发分析接口 +2. 提供 GET /api/v1/analysis/status/{task_id} 查询任务状态接口 +3. 提供 GET /api/v1/analysis/tasks 获取任务列表接口 +4. 提供 GET /api/v1/analysis/tasks/stream SSE 实时推送接口 + +特性: +- 异步任务队列:分析任务异步执行,不阻塞请求 +- 防重复提交:相同股票代码正在分析时返回 409 +- SSE 实时推送:任务状态变化实时通知前端 +""" + +import asyncio +import json +import logging +from datetime import datetime +from typing import Optional, Union, Dict, Any + +from fastapi import APIRouter, HTTPException, Depends, Query +from fastapi.responses import JSONResponse, StreamingResponse + +from api.deps import get_config_dep +from api.v1.schemas.analysis import ( + AnalyzeRequest, + AnalysisResultResponse, + TaskAccepted, + TaskStatus, + TaskInfo, + TaskListResponse, + DuplicateTaskErrorResponse, +) +from api.v1.schemas.common import ErrorResponse +from api.v1.schemas.history import ( + AnalysisReport, + ReportMeta, + ReportSummary, + ReportStrategy, + ReportDetails, +) +from src.config import Config +from src.services.task_queue import ( + get_task_queue, + DuplicateTaskError, + TaskStatus as TaskStatusEnum, +) + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +# ============================================================ +# POST /analyze - 触发股票分析 +# ============================================================ + +@router.post( + "/analyze", + response_model=AnalysisResultResponse, + responses={ + 200: {"description": "分析完成(同步模式)", "model": AnalysisResultResponse}, + 202: {"description": "分析任务已接受(异步模式)", "model": TaskAccepted}, + 400: {"description": "请求参数错误", "model": ErrorResponse}, + 409: {"description": "股票正在分析中,拒绝重复提交", "model": DuplicateTaskErrorResponse}, + 500: {"description": "分析失败", "model": ErrorResponse}, + }, + summary="触发股票分析", + description="启动 AI 智能分析任务,支持同步和异步模式。异步模式下相同股票代码不允许重复提交。" +) +def trigger_analysis( + request: AnalyzeRequest, + config: Config = Depends(get_config_dep) +) -> Union[AnalysisResultResponse, JSONResponse]: + """ + 触发股票分析 + + 启动 AI 智能分析任务,支持单只或多只股票批量分析 + + 流程: + 1. 校验请求参数 + 2. 异步模式:检查重复 -> 提交任务队列 -> 返回 202 + 3. 同步模式:直接执行分析 -> 返回 200 + + Args: + request: 分析请求参数 + config: 配置依赖 + + Returns: + AnalysisResultResponse: 分析结果(同步模式) + TaskAccepted: 任务已接受(异步模式,返回 202) + + Raises: + HTTPException: 400 - 请求参数错误 + HTTPException: 409 - 股票正在分析中 + HTTPException: 500 - 分析失败 + """ + # 校验请求参数 + stock_codes = [] + if request.stock_code: + stock_codes.append(request.stock_code) + if request.stock_codes: + stock_codes.extend(request.stock_codes) + + if not stock_codes: + raise HTTPException( + status_code=400, + detail={ + "error": "validation_error", + "message": "必须提供 stock_code 或 stock_codes 参数" + } + ) + + # 去重 + stock_codes = list(dict.fromkeys(stock_codes)) + stock_code = stock_codes[0] # 当前只处理第一个 + + # 异步模式:使用任务队列 + if request.async_mode: + return _handle_async_analysis(stock_code, request) + + # 同步模式:直接执行分析 + return _handle_sync_analysis(stock_code, request) + + +def _handle_async_analysis( + stock_code: str, + request: AnalyzeRequest +) -> JSONResponse: + """ + 处理异步分析请求 + + 提交任务到队列,立即返回 202 + 如果股票正在分析中,返回 409 + """ + task_queue = get_task_queue() + + try: + # 提交任务(如果重复会抛出 DuplicateTaskError) + task_info = task_queue.submit_task( + stock_code=stock_code, + stock_name=None, # 名称在分析过程中获取 + report_type=request.report_type, + force_refresh=request.force_refresh, + ) + + # 返回 202 Accepted + task_accepted = TaskAccepted( + task_id=task_info.task_id, + status="pending", + message=f"分析任务已加入队列: {stock_code}" + ) + return JSONResponse( + status_code=202, + content=task_accepted.model_dump() + ) + + except DuplicateTaskError as e: + # 股票正在分析中,返回 409 Conflict + error_response = DuplicateTaskErrorResponse( + error="duplicate_task", + message=str(e), + stock_code=e.stock_code, + existing_task_id=e.existing_task_id, + ) + return JSONResponse( + status_code=409, + content=error_response.model_dump() + ) + + +def _handle_sync_analysis( + stock_code: str, + request: AnalyzeRequest +) -> AnalysisResultResponse: + """ + 处理同步分析请求 + + 直接执行分析,等待完成后返回结果 + """ + import uuid + from src.services.analysis_service import AnalysisService + + query_id = uuid.uuid4().hex + + try: + service = AnalysisService() + result = service.analyze_stock( + stock_code=stock_code, + report_type=request.report_type, + force_refresh=request.force_refresh, + query_id=query_id + ) + + if result is None: + raise HTTPException( + status_code=500, + detail={ + "error": "analysis_failed", + "message": f"分析股票 {stock_code} 失败" + } + ) + + # 构建报告结构 + report_data = result.get("report", {}) + report = _build_analysis_report( + report_data, query_id, stock_code, result.get("stock_name") + ) + + return AnalysisResultResponse( + query_id=query_id, + stock_code=result.get("stock_code", stock_code), + stock_name=result.get("stock_name"), + report=report.model_dump() if report else None, + created_at=datetime.now().isoformat() + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"分析失败: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail={ + "error": "internal_error", + "message": f"分析过程发生错误: {str(e)}" + } + ) + + +# ============================================================ +# GET /tasks - 获取任务列表 +# ============================================================ + +@router.get( + "/tasks", + response_model=TaskListResponse, + responses={ + 200: {"description": "任务列表"}, + }, + summary="获取分析任务列表", + description="获取当前所有分析任务,可按状态筛选" +) +def get_task_list( + status: Optional[str] = Query( + None, + description="筛选状态:pending, processing, completed, failed(支持逗号分隔多个)" + ), + limit: int = Query(20, description="返回数量限制", ge=1, le=100), +) -> TaskListResponse: + """ + 获取分析任务列表 + + Args: + status: 状态筛选(可选) + limit: 返回数量限制 + + Returns: + TaskListResponse: 任务列表响应 + """ + task_queue = get_task_queue() + + # 获取所有任务 + all_tasks = task_queue.list_all_tasks(limit=limit) + + # 状态筛选 + if status: + status_list = [s.strip().lower() for s in status.split(",")] + all_tasks = [t for t in all_tasks if t.status.value in status_list] + + # 统计信息 + stats = task_queue.get_task_stats() + + # 转换为 Schema + task_infos = [ + TaskInfo( + task_id=t.task_id, + stock_code=t.stock_code, + stock_name=t.stock_name, + status=t.status.value, + progress=t.progress, + message=t.message, + report_type=t.report_type, + created_at=t.created_at.isoformat(), + started_at=t.started_at.isoformat() if t.started_at else None, + completed_at=t.completed_at.isoformat() if t.completed_at else None, + error=t.error, + ) + for t in all_tasks + ] + + return TaskListResponse( + total=stats["total"], + pending=stats["pending"], + processing=stats["processing"], + tasks=task_infos, + ) + + +# ============================================================ +# GET /tasks/stream - SSE 实时推送 +# ============================================================ + +@router.get( + "/tasks/stream", + responses={ + 200: {"description": "SSE 事件流", "content": {"text/event-stream": {}}}, + }, + summary="任务状态 SSE 流", + description="通过 Server-Sent Events 实时推送任务状态变化" +) +async def task_stream(): + """ + SSE 任务状态流 + + 事件类型: + - connected: 连接成功 + - task_created: 新任务创建 + - task_started: 任务开始执行 + - task_completed: 任务完成 + - task_failed: 任务失败 + - heartbeat: 心跳(每 30 秒) + + Returns: + StreamingResponse: SSE 事件流 + """ + async def event_generator(): + task_queue = get_task_queue() + event_queue: asyncio.Queue = asyncio.Queue() + + # 发送连接成功事件 + yield _format_sse_event("connected", {"message": "Connected to task stream"}) + + # 发送当前进行中的任务 + pending_tasks = task_queue.list_pending_tasks() + for task in pending_tasks: + yield _format_sse_event("task_created", task.to_dict()) + + # 订阅任务事件 + task_queue.subscribe(event_queue) + + try: + while True: + try: + # 等待事件,超时发送心跳 + event = await asyncio.wait_for(event_queue.get(), timeout=30) + yield _format_sse_event(event["type"], event["data"]) + except asyncio.TimeoutError: + # 心跳 + yield _format_sse_event("heartbeat", { + "timestamp": datetime.now().isoformat() + }) + except asyncio.CancelledError: + # 客户端断开连接 + pass + finally: + task_queue.unsubscribe(event_queue) + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Accel-Buffering": "no", # 禁用 Nginx 缓冲 + } + ) + + +def _format_sse_event(event_type: str, data: Dict[str, Any]) -> str: + """ + 格式化 SSE 事件 + + Args: + event_type: 事件类型 + data: 事件数据 + + Returns: + SSE 格式字符串 + """ + return f"event: {event_type}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n" + + +# ============================================================ +# GET /status/{task_id} - 查询单个任务状态 +# ============================================================ + +@router.get( + "/status/{task_id}", + response_model=TaskStatus, + responses={ + 200: {"description": "任务状态"}, + 404: {"description": "任务不存在", "model": ErrorResponse}, + }, + summary="查询分析任务状态", + description="根据 task_id 查询单个任务的状态" +) +def get_analysis_status(task_id: str) -> TaskStatus: + """ + 查询分析任务状态 + + 优先从任务队列查询,如果不存在则从数据库查询历史记录 + + Args: + task_id: 任务 ID + + Returns: + TaskStatus: 任务状态信息 + + Raises: + HTTPException: 404 - 任务不存在 + """ + # 1. 先从任务队列查询 + task_queue = get_task_queue() + task = task_queue.get_task(task_id) + + if task: + return TaskStatus( + task_id=task.task_id, + status=task.status.value, + progress=task.progress, + result=None, # 进行中的任务没有结果 + error=task.error, + ) + + # 2. 从数据库查询已完成的记录 + try: + from src.storage import DatabaseManager + db = DatabaseManager.get_instance() + records = db.get_analysis_history(query_id=task_id, limit=1) + + if records: + record = records[0] + # Build report from DB record so completed tasks return real data + report_dict = AnalysisReport( + meta=ReportMeta( + query_id=task_id, + stock_code=record.code, + stock_name=record.name, + report_type=getattr(record, 'report_type', None), + created_at=record.created_at.isoformat() if record.created_at else None, + ), + summary=ReportSummary( + sentiment_score=record.sentiment_score, + operation_advice=record.operation_advice, + trend_prediction=record.trend_prediction, + analysis_summary=record.analysis_summary, + ), + strategy=ReportStrategy( + ideal_buy=str(getattr(record, 'ideal_buy', None)) if getattr(record, 'ideal_buy', None) is not None else None, + secondary_buy=str(getattr(record, 'secondary_buy', None)) if getattr(record, 'secondary_buy', None) is not None else None, + stop_loss=str(getattr(record, 'stop_loss', None)) if getattr(record, 'stop_loss', None) is not None else None, + take_profit=str(getattr(record, 'take_profit', None)) if getattr(record, 'take_profit', None) is not None else None, + ), + ).model_dump() + return TaskStatus( + task_id=task_id, + status="completed", + progress=100, + result=AnalysisResultResponse( + query_id=task_id, + stock_code=record.code, + stock_name=record.name, + report=report_dict, + created_at=record.created_at.isoformat() if record.created_at else datetime.now().isoformat() + ), + error=None + ) + + except Exception as e: + logger.error(f"查询任务状态失败: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail={ + "error": "internal_error", + "message": f"查询任务状态失败: {str(e)}" + } + ) + + # 3. 任务不存在 + raise HTTPException( + status_code=404, + detail={ + "error": "not_found", + "message": f"任务 {task_id} 不存在或已过期" + } + ) + + +# ============================================================ +# 辅助函数 +# ============================================================ + +def _build_analysis_report( + report_data: Dict[str, Any], + query_id: str, + stock_code: str, + stock_name: Optional[str] = None +) -> AnalysisReport: + """ + 构建符合 API 规范的分析报告 + + Args: + report_data: 原始报告数据 + query_id: 查询 ID + stock_code: 股票代码 + stock_name: 股票名称 + + Returns: + AnalysisReport: 结构化的分析报告 + """ + meta_data = report_data.get("meta", {}) + summary_data = report_data.get("summary", {}) + strategy_data = report_data.get("strategy", {}) + details_data = report_data.get("details", {}) + + meta = ReportMeta( + query_id=meta_data.get("query_id", query_id), + stock_code=meta_data.get("stock_code", stock_code), + stock_name=meta_data.get("stock_name", stock_name), + report_type=meta_data.get("report_type", "detailed"), + created_at=meta_data.get("created_at", datetime.now().isoformat()), + current_price=meta_data.get("current_price"), + change_pct=meta_data.get("change_pct"), + ) + + summary = ReportSummary( + analysis_summary=summary_data.get("analysis_summary"), + operation_advice=summary_data.get("operation_advice"), + trend_prediction=summary_data.get("trend_prediction"), + sentiment_score=summary_data.get("sentiment_score"), + sentiment_label=summary_data.get("sentiment_label") + ) + + strategy = None + if strategy_data: + strategy = ReportStrategy( + ideal_buy=strategy_data.get("ideal_buy"), + secondary_buy=strategy_data.get("secondary_buy"), + stop_loss=strategy_data.get("stop_loss"), + take_profit=strategy_data.get("take_profit") + ) + + details = None + if details_data: + details = ReportDetails( + news_content=details_data.get("news_summary") or details_data.get("news_content"), + raw_result=details_data, + context_snapshot=None + ) + + return AnalysisReport( + meta=meta, + summary=summary, + strategy=strategy, + details=details + ) diff --git a/api/v1/endpoints/backtest.py b/api/v1/endpoints/backtest.py new file mode 100644 index 0000000..1001e40 --- /dev/null +++ b/api/v1/endpoints/backtest.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +"""Backtest endpoints.""" + +from __future__ import annotations + +import logging +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, Query + +from api.deps import get_database_manager +from api.v1.schemas.backtest import ( + BacktestRunRequest, + BacktestRunResponse, + BacktestResultItem, + BacktestResultsResponse, + PerformanceMetrics, +) +from api.v1.schemas.common import ErrorResponse +from src.services.backtest_service import BacktestService +from src.storage import DatabaseManager + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +@router.post( + "/run", + response_model=BacktestRunResponse, + responses={ + 200: {"description": "回测执行完成"}, + 500: {"description": "服务器错误", "model": ErrorResponse}, + }, + summary="触发回测", + description="对历史分析记录进行回测评估,并写入 backtest_results/backtest_summaries", +) +def run_backtest( + request: BacktestRunRequest, + db_manager: DatabaseManager = Depends(get_database_manager), +) -> BacktestRunResponse: + try: + service = BacktestService(db_manager) + stats = service.run_backtest( + code=request.code, + force=request.force, + eval_window_days=request.eval_window_days, + min_age_days=request.min_age_days, + limit=request.limit, + ) + return BacktestRunResponse(**stats) + except Exception as exc: + logger.error(f"回测执行失败: {exc}", exc_info=True) + raise HTTPException( + status_code=500, + detail={"error": "internal_error", "message": f"回测执行失败: {str(exc)}"}, + ) + + +@router.get( + "/results", + response_model=BacktestResultsResponse, + responses={ + 200: {"description": "回测结果列表"}, + 500: {"description": "服务器错误", "model": ErrorResponse}, + }, + summary="获取回测结果", + description="分页获取回测结果,支持按股票代码过滤", +) +def get_backtest_results( + code: Optional[str] = Query(None, description="股票代码筛选"), + eval_window_days: Optional[int] = Query(None, ge=1, le=120, description="评估窗口过滤"), + page: int = Query(1, ge=1, description="页码"), + limit: int = Query(20, ge=1, le=200, description="每页数量"), + db_manager: DatabaseManager = Depends(get_database_manager), +) -> BacktestResultsResponse: + try: + service = BacktestService(db_manager) + data = service.get_recent_evaluations(code=code, eval_window_days=eval_window_days, limit=limit, page=page) + items = [BacktestResultItem(**item) for item in data.get("items", [])] + return BacktestResultsResponse( + total=int(data.get("total", 0)), + page=page, + limit=limit, + items=items, + ) + except Exception as exc: + logger.error(f"查询回测结果失败: {exc}", exc_info=True) + raise HTTPException( + status_code=500, + detail={"error": "internal_error", "message": f"查询回测结果失败: {str(exc)}"}, + ) + + +@router.get( + "/performance", + response_model=PerformanceMetrics, + responses={ + 200: {"description": "整体回测表现"}, + 404: {"description": "无回测汇总", "model": ErrorResponse}, + 500: {"description": "服务器错误", "model": ErrorResponse}, + }, + summary="获取整体回测表现", +) +def get_overall_performance( + eval_window_days: Optional[int] = Query(None, ge=1, le=120, description="评估窗口过滤"), + db_manager: DatabaseManager = Depends(get_database_manager), +) -> PerformanceMetrics: + try: + service = BacktestService(db_manager) + summary = service.get_summary(scope="overall", code=None, eval_window_days=eval_window_days) + if summary is None: + raise HTTPException( + status_code=404, + detail={"error": "not_found", "message": "未找到整体回测汇总"}, + ) + return PerformanceMetrics(**summary) + except HTTPException: + raise + except Exception as exc: + logger.error(f"查询整体表现失败: {exc}", exc_info=True) + raise HTTPException( + status_code=500, + detail={"error": "internal_error", "message": f"查询整体表现失败: {str(exc)}"}, + ) + + +@router.get( + "/performance/{code}", + response_model=PerformanceMetrics, + responses={ + 200: {"description": "单股回测表现"}, + 404: {"description": "无回测汇总", "model": ErrorResponse}, + 500: {"description": "服务器错误", "model": ErrorResponse}, + }, + summary="获取单股回测表现", +) +def get_stock_performance( + code: str, + eval_window_days: Optional[int] = Query(None, ge=1, le=120, description="评估窗口过滤"), + db_manager: DatabaseManager = Depends(get_database_manager), +) -> PerformanceMetrics: + try: + service = BacktestService(db_manager) + summary = service.get_summary(scope="stock", code=code, eval_window_days=eval_window_days) + if summary is None: + raise HTTPException( + status_code=404, + detail={"error": "not_found", "message": f"未找到 {code} 的回测汇总"}, + ) + return PerformanceMetrics(**summary) + except HTTPException: + raise + except Exception as exc: + logger.error(f"查询单股表现失败: {exc}", exc_info=True) + raise HTTPException( + status_code=500, + detail={"error": "internal_error", "message": f"查询单股表现失败: {str(exc)}"}, + ) + diff --git a/api/v1/endpoints/health.py b/api/v1/endpoints/health.py new file mode 100644 index 0000000..78b4ccb --- /dev/null +++ b/api/v1/endpoints/health.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +""" +=================================== +健康检查接口 +=================================== + +职责: +1. 提供 /api/v1/health 健康检查接口 +2. 用于负载均衡器和监控系统 +""" + +from datetime import datetime + +from fastapi import APIRouter + +from api.v1.schemas.common import HealthResponse + +router = APIRouter() + + +@router.get("/health", response_model=HealthResponse) +async def health_check() -> HealthResponse: + """ + 健康检查接口 + + 用于负载均衡器或监控系统检查服务状态 + + Returns: + HealthResponse: 包含服务状态和时间戳 + """ + return HealthResponse( + status="ok", + timestamp=datetime.now().isoformat() + ) diff --git a/api/v1/endpoints/history.py b/api/v1/endpoints/history.py new file mode 100644 index 0000000..f7d7a20 --- /dev/null +++ b/api/v1/endpoints/history.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- +""" +=================================== +历史记录接口 +=================================== + +职责: +1. 提供 GET /api/v1/history 历史列表查询接口 +2. 提供 GET /api/v1/history/{query_id} 历史详情查询接口 +""" + +import logging +from typing import Optional + +from fastapi import APIRouter, HTTPException, Query, Depends + +from api.deps import get_database_manager +from api.v1.schemas.history import ( + HistoryListResponse, + HistoryItem, + NewsIntelItem, + NewsIntelResponse, + AnalysisReport, + ReportMeta, + ReportSummary, + ReportStrategy, + ReportDetails, +) +from api.v1.schemas.common import ErrorResponse +from src.storage import DatabaseManager +from src.services.history_service import HistoryService + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +@router.get( + "", + response_model=HistoryListResponse, + responses={ + 200: {"description": "历史记录列表"}, + 500: {"description": "服务器错误", "model": ErrorResponse}, + }, + summary="获取历史分析列表", + description="分页获取历史分析记录摘要,支持按股票代码和日期范围筛选" +) +def get_history_list( + stock_code: Optional[str] = Query(None, description="股票代码筛选"), + start_date: Optional[str] = Query(None, description="开始日期 (YYYY-MM-DD)"), + end_date: Optional[str] = Query(None, description="结束日期 (YYYY-MM-DD)"), + page: int = Query(1, ge=1, description="页码(从 1 开始)"), + limit: int = Query(20, ge=1, le=100, description="每页数量"), + db_manager: DatabaseManager = Depends(get_database_manager) +) -> HistoryListResponse: + """ + 获取历史分析列表 + + 分页获取历史分析记录摘要,支持按股票代码和日期范围筛选 + + Args: + stock_code: 股票代码筛选 + start_date: 开始日期 + end_date: 结束日期 + page: 页码 + limit: 每页数量 + db_manager: 数据库管理器依赖 + + Returns: + HistoryListResponse: 历史记录列表 + """ + try: + service = HistoryService(db_manager) + + # 使用 def 而非 async def,FastAPI 自动在线程池中执行 + result = service.get_history_list( + stock_code=stock_code, + start_date=start_date, + end_date=end_date, + page=page, + limit=limit + ) + + # 转换为响应模型 + items = [ + HistoryItem( + query_id=item.get("query_id", ""), + stock_code=item.get("stock_code", ""), + stock_name=item.get("stock_name"), + report_type=item.get("report_type"), + sentiment_score=item.get("sentiment_score"), + operation_advice=item.get("operation_advice"), + created_at=item.get("created_at") + ) + for item in result.get("items", []) + ] + + return HistoryListResponse( + total=result.get("total", 0), + page=page, + limit=limit, + items=items + ) + + except Exception as e: + logger.error(f"查询历史列表失败: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail={ + "error": "internal_error", + "message": f"查询历史列表失败: {str(e)}" + } + ) + + +@router.get( + "/{query_id}", + response_model=AnalysisReport, + responses={ + 200: {"description": "报告详情"}, + 404: {"description": "报告不存在", "model": ErrorResponse}, + 500: {"description": "服务器错误", "model": ErrorResponse}, + }, + summary="获取历史报告详情", + description="根据 query_id 获取完整的历史分析报告" +) +def get_history_detail( + query_id: str, + db_manager: DatabaseManager = Depends(get_database_manager) +) -> AnalysisReport: + """ + 获取历史报告详情 + + 根据 query_id 获取完整的历史分析报告 + + Args: + query_id: 分析记录唯一标识 + db_manager: 数据库管理器依赖 + + Returns: + AnalysisReport: 完整分析报告 + + Raises: + HTTPException: 404 - 报告不存在 + """ + try: + service = HistoryService(db_manager) + + # 使用 def 而非 async def,FastAPI 自动在线程池中执行 + result = service.get_history_detail(query_id) + + if result is None: + raise HTTPException( + status_code=404, + detail={ + "error": "not_found", + "message": f"未找到 query_id={query_id} 的分析记录" + } + ) + + # 从 context_snapshot 中提取价格信息 + current_price = None + change_pct = None + context_snapshot = result.get("context_snapshot") + if context_snapshot and isinstance(context_snapshot, dict): + # 尝试从 enhanced_context.realtime 获取 + enhanced_context = context_snapshot.get("enhanced_context") or {} + realtime = enhanced_context.get("realtime") or {} + current_price = realtime.get("price") + change_pct = realtime.get("change_pct") or realtime.get("change_60d") + + # 也尝试从 realtime_quote_raw 获取 + if current_price is None: + realtime_quote_raw = context_snapshot.get("realtime_quote_raw") or {} + current_price = realtime_quote_raw.get("price") + change_pct = change_pct or realtime_quote_raw.get("change_pct") or realtime_quote_raw.get("pct_chg") + + # 构建响应模型 + meta = ReportMeta( + query_id=result.get("query_id", query_id), + stock_code=result.get("stock_code", ""), + stock_name=result.get("stock_name"), + report_type=result.get("report_type"), + created_at=result.get("created_at"), + current_price=current_price, + change_pct=change_pct + ) + + summary = ReportSummary( + analysis_summary=result.get("analysis_summary"), + operation_advice=result.get("operation_advice"), + trend_prediction=result.get("trend_prediction"), + sentiment_score=result.get("sentiment_score"), + sentiment_label=result.get("sentiment_label") + ) + + strategy = ReportStrategy( + ideal_buy=result.get("ideal_buy"), + secondary_buy=result.get("secondary_buy"), + stop_loss=result.get("stop_loss"), + take_profit=result.get("take_profit") + ) + + details = ReportDetails( + news_content=result.get("news_content"), + raw_result=result.get("raw_result"), + context_snapshot=result.get("context_snapshot") + ) + + return AnalysisReport( + meta=meta, + summary=summary, + strategy=strategy, + details=details + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"查询历史详情失败: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail={ + "error": "internal_error", + "message": f"查询历史详情失败: {str(e)}" + } + ) + + +@router.get( + "/{query_id}/news", + response_model=NewsIntelResponse, + responses={ + 200: {"description": "新闻情报列表"}, + 500: {"description": "服务器错误", "model": ErrorResponse}, + }, + summary="获取历史报告关联新闻", + description="根据 query_id 获取关联的新闻情报列表(为空也返回 200)" +) +def get_history_news( + query_id: str, + limit: int = Query(20, ge=1, le=100, description="返回数量限制"), + db_manager: DatabaseManager = Depends(get_database_manager) +) -> NewsIntelResponse: + """ + 获取历史报告关联新闻 + + Args: + query_id: 分析记录唯一标识 + limit: 返回数量限制 + db_manager: 数据库管理器依赖 + + Returns: + NewsIntelResponse: 新闻情报列表 + """ + try: + service = HistoryService(db_manager) + items = service.get_news_intel(query_id=query_id, limit=limit) + + response_items = [ + NewsIntelItem( + title=item.get("title", ""), + snippet=item.get("snippet"), + url=item.get("url", "") + ) + for item in items + ] + + return NewsIntelResponse( + total=len(response_items), + items=response_items + ) + + except Exception as e: + logger.error(f"查询新闻情报失败: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail={ + "error": "internal_error", + "message": f"查询新闻情报失败: {str(e)}" + } + ) diff --git a/api/v1/endpoints/stocks.py b/api/v1/endpoints/stocks.py new file mode 100644 index 0000000..5b78953 --- /dev/null +++ b/api/v1/endpoints/stocks.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +""" +=================================== +股票数据接口 +=================================== + +职责: +1. 提供 GET /api/v1/stocks/{code}/quote 实时行情接口 +2. 提供 GET /api/v1/stocks/{code}/history 历史行情接口 +""" + +import logging + +from fastapi import APIRouter, HTTPException, Query + +from api.v1.schemas.stocks import ( + StockQuote, + StockHistoryResponse, + KLineData, +) +from api.v1.schemas.common import ErrorResponse +from src.services.stock_service import StockService + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +@router.get( + "/{stock_code}/quote", + response_model=StockQuote, + responses={ + 200: {"description": "行情数据"}, + 404: {"description": "股票不存在", "model": ErrorResponse}, + 500: {"description": "服务器错误", "model": ErrorResponse}, + }, + summary="获取股票实时行情", + description="获取指定股票的最新行情数据" +) +def get_stock_quote(stock_code: str) -> StockQuote: + """ + 获取股票实时行情 + + 获取指定股票的最新行情数据 + + Args: + stock_code: 股票代码(如 600519、00700、AAPL) + + Returns: + StockQuote: 实时行情数据 + + Raises: + HTTPException: 404 - 股票不存在 + """ + try: + service = StockService() + + # 使用 def 而非 async def,FastAPI 自动在线程池中执行 + result = service.get_realtime_quote(stock_code) + + if result is None: + raise HTTPException( + status_code=404, + detail={ + "error": "not_found", + "message": f"未找到股票 {stock_code} 的行情数据" + } + ) + + return StockQuote( + stock_code=result.get("stock_code", stock_code), + stock_name=result.get("stock_name"), + current_price=result.get("current_price", 0.0), + change=result.get("change"), + change_percent=result.get("change_percent"), + open=result.get("open"), + high=result.get("high"), + low=result.get("low"), + prev_close=result.get("prev_close"), + volume=result.get("volume"), + amount=result.get("amount"), + update_time=result.get("update_time") + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取实时行情失败: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail={ + "error": "internal_error", + "message": f"获取实时行情失败: {str(e)}" + } + ) + + +@router.get( + "/{stock_code}/history", + response_model=StockHistoryResponse, + responses={ + 200: {"description": "历史行情数据"}, + 422: {"description": "不支持的周期参数", "model": ErrorResponse}, + 500: {"description": "服务器错误", "model": ErrorResponse}, + }, + summary="获取股票历史行情", + description="获取指定股票的历史 K 线数据" +) +def get_stock_history( + stock_code: str, + period: str = Query("daily", description="K 线周期", pattern="^(daily|weekly|monthly)$"), + days: int = Query(30, ge=1, le=365, description="获取天数") +) -> StockHistoryResponse: + """ + 获取股票历史行情 + + 获取指定股票的历史 K 线数据 + + Args: + stock_code: 股票代码 + period: K 线周期 (daily/weekly/monthly) + days: 获取天数 + + Returns: + StockHistoryResponse: 历史行情数据 + """ + try: + service = StockService() + + # 使用 def 而非 async def,FastAPI 自动在线程池中执行 + result = service.get_history_data( + stock_code=stock_code, + period=period, + days=days + ) + + # 转换为响应模型 + data = [ + KLineData( + date=item.get("date"), + open=item.get("open"), + high=item.get("high"), + low=item.get("low"), + close=item.get("close"), + volume=item.get("volume"), + amount=item.get("amount"), + change_percent=item.get("change_percent") + ) + for item in result.get("data", []) + ] + + return StockHistoryResponse( + stock_code=stock_code, + stock_name=result.get("stock_name"), + period=period, + data=data + ) + + except ValueError as e: + # period 参数不支持的错误(如 weekly/monthly) + raise HTTPException( + status_code=422, + detail={ + "error": "unsupported_period", + "message": str(e) + } + ) + except Exception as e: + logger.error(f"获取历史行情失败: {e}", exc_info=True) + raise HTTPException( + status_code=500, + detail={ + "error": "internal_error", + "message": f"获取历史行情失败: {str(e)}" + } + ) diff --git a/api/v1/endpoints/system_config.py b/api/v1/endpoints/system_config.py new file mode 100644 index 0000000..05bed4e --- /dev/null +++ b/api/v1/endpoints/system_config.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +"""System configuration endpoints.""" + +from __future__ import annotations + +import logging + +from fastapi import APIRouter, Depends, HTTPException, Query + +from api.deps import get_system_config_service +from api.v1.schemas.common import ErrorResponse +from api.v1.schemas.system_config import ( + SystemConfigConflictResponse, + SystemConfigResponse, + SystemConfigSchemaResponse, + SystemConfigValidationErrorResponse, + UpdateSystemConfigRequest, + UpdateSystemConfigResponse, + ValidateSystemConfigRequest, + ValidateSystemConfigResponse, +) +from src.services.system_config_service import ConfigConflictError, ConfigValidationError, SystemConfigService + +logger = logging.getLogger(__name__) + +router = APIRouter() + + +@router.get( + "/config", + response_model=SystemConfigResponse, + responses={ + 200: {"description": "Configuration loaded"}, + 401: {"description": "Unauthorized", "model": ErrorResponse}, + 500: {"description": "Internal server error", "model": ErrorResponse}, + }, + summary="Get system configuration", + description="Read current configuration from .env and return raw values.", +) +def get_system_config( + include_schema: bool = Query(True, description="Whether to include schema metadata"), + service: SystemConfigService = Depends(get_system_config_service), +) -> SystemConfigResponse: + """Load and return current system configuration.""" + try: + payload = service.get_config(include_schema=include_schema) + return SystemConfigResponse.model_validate(payload) + except Exception as exc: + logger.error("Failed to load system configuration: %s", exc, exc_info=True) + raise HTTPException( + status_code=500, + detail={ + "error": "internal_error", + "message": "Failed to load system configuration", + }, + ) + + +@router.put( + "/config", + response_model=UpdateSystemConfigResponse, + responses={ + 200: {"description": "Configuration updated"}, + 400: {"description": "Validation failed", "model": SystemConfigValidationErrorResponse}, + 409: {"description": "Version conflict", "model": SystemConfigConflictResponse}, + 500: {"description": "Internal server error", "model": ErrorResponse}, + }, + summary="Update system configuration", + description="Update key-value pairs in .env. Mask token preserves existing secret values.", +) +def update_system_config( + request: UpdateSystemConfigRequest, + service: SystemConfigService = Depends(get_system_config_service), +) -> UpdateSystemConfigResponse: + """Validate and persist system configuration updates.""" + try: + payload = service.update( + config_version=request.config_version, + items=[item.model_dump() for item in request.items], + mask_token=request.mask_token, + reload_now=request.reload_now, + ) + return UpdateSystemConfigResponse.model_validate(payload) + except ConfigValidationError as exc: + raise HTTPException( + status_code=400, + detail={ + "error": "validation_failed", + "message": "System configuration validation failed", + "issues": exc.issues, + }, + ) + except ConfigConflictError as exc: + raise HTTPException( + status_code=409, + detail={ + "error": "config_version_conflict", + "message": "Configuration has changed, please reload and retry", + "current_config_version": exc.current_version, + }, + ) + except Exception as exc: + logger.error("Failed to update system configuration: %s", exc, exc_info=True) + raise HTTPException( + status_code=500, + detail={ + "error": "internal_error", + "message": "Failed to update system configuration", + }, + ) + + +@router.post( + "/config/validate", + response_model=ValidateSystemConfigResponse, + responses={ + 200: {"description": "Validation completed"}, + 500: {"description": "Internal server error", "model": ErrorResponse}, + }, + summary="Validate system configuration", + description="Validate submitted configuration values without writing to .env.", +) +def validate_system_config( + request: ValidateSystemConfigRequest, + service: SystemConfigService = Depends(get_system_config_service), +) -> ValidateSystemConfigResponse: + """Run pre-save validation only.""" + try: + payload = service.validate(items=[item.model_dump() for item in request.items]) + return ValidateSystemConfigResponse.model_validate(payload) + except Exception as exc: + logger.error("Failed to validate system configuration: %s", exc, exc_info=True) + raise HTTPException( + status_code=500, + detail={ + "error": "internal_error", + "message": "Failed to validate system configuration", + }, + ) + + +@router.get( + "/config/schema", + response_model=SystemConfigSchemaResponse, + responses={ + 200: {"description": "Schema loaded"}, + 500: {"description": "Internal server error", "model": ErrorResponse}, + }, + summary="Get system configuration schema", + description="Return categorized field metadata used for dynamic settings form rendering.", +) +def get_system_config_schema( + service: SystemConfigService = Depends(get_system_config_service), +) -> SystemConfigSchemaResponse: + """Return schema metadata for system configuration fields.""" + try: + payload = service.get_schema() + return SystemConfigSchemaResponse.model_validate(payload) + except Exception as exc: + logger.error("Failed to load system configuration schema: %s", exc, exc_info=True) + raise HTTPException( + status_code=500, + detail={ + "error": "internal_error", + "message": "Failed to load system configuration schema", + }, + ) diff --git a/api/v1/router.py b/api/v1/router.py new file mode 100644 index 0000000..b311740 --- /dev/null +++ b/api/v1/router.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" +=================================== +API v1 路由聚合 +=================================== + +职责: +1. 聚合 v1 版本的所有 endpoint 路由 +2. 统一添加 /api/v1 前缀 +""" + +from fastapi import APIRouter + +from api.v1.endpoints import analysis, history, stocks, backtest, system_config + +# 创建 v1 版本主路由 +router = APIRouter(prefix="/api/v1") + +router.include_router( + analysis.router, + prefix="/analysis", + tags=["Analysis"] +) + +router.include_router( + history.router, + prefix="/history", + tags=["History"] +) + +router.include_router( + stocks.router, + prefix="/stocks", + tags=["Stocks"] +) + +router.include_router( + backtest.router, + prefix="/backtest", + tags=["Backtest"] +) + +router.include_router( + system_config.router, + prefix="/system", + tags=["SystemConfig"] +) diff --git a/api/v1/schemas/__init__.py b/api/v1/schemas/__init__.py new file mode 100644 index 0000000..09e8bc8 --- /dev/null +++ b/api/v1/schemas/__init__.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +""" +=================================== +API v1 Schemas 模块初始化 +=================================== + +职责: +1. 导出所有 Pydantic 模型 +""" + +from api.v1.schemas.common import ( + RootResponse, + HealthResponse, + ErrorResponse, + SuccessResponse, +) +from api.v1.schemas.analysis import ( + AnalyzeRequest, + AnalysisResultResponse, + TaskAccepted, + TaskStatus, +) +from api.v1.schemas.history import ( + HistoryItem, + HistoryListResponse, + NewsIntelItem, + NewsIntelResponse, + AnalysisReport, + ReportMeta, + ReportSummary, + ReportStrategy, + ReportDetails, +) +from api.v1.schemas.stocks import ( + StockQuote, + StockHistoryResponse, + KLineData, +) +from api.v1.schemas.backtest import ( + BacktestRunRequest, + BacktestRunResponse, + BacktestResultItem, + BacktestResultsResponse, + PerformanceMetrics, +) +from api.v1.schemas.system_config import ( + SystemConfigFieldSchema, + SystemConfigCategorySchema, + SystemConfigSchemaResponse, + SystemConfigItem, + SystemConfigResponse, + SystemConfigUpdateItem, + UpdateSystemConfigRequest, + UpdateSystemConfigResponse, + ValidateSystemConfigRequest, + ConfigValidationIssue, + ValidateSystemConfigResponse, + SystemConfigValidationErrorResponse, + SystemConfigConflictResponse, +) + +__all__ = [ + # common + "RootResponse", + "HealthResponse", + "ErrorResponse", + "SuccessResponse", + # analysis + "AnalyzeRequest", + "AnalysisResultResponse", + "TaskAccepted", + "TaskStatus", + # history + "HistoryItem", + "HistoryListResponse", + "NewsIntelItem", + "NewsIntelResponse", + "AnalysisReport", + "ReportMeta", + "ReportSummary", + "ReportStrategy", + "ReportDetails", + # stocks + "StockQuote", + "StockHistoryResponse", + "KLineData", + # backtest + "BacktestRunRequest", + "BacktestRunResponse", + "BacktestResultItem", + "BacktestResultsResponse", + "PerformanceMetrics", + # system config + "SystemConfigFieldSchema", + "SystemConfigCategorySchema", + "SystemConfigSchemaResponse", + "SystemConfigItem", + "SystemConfigResponse", + "SystemConfigUpdateItem", + "UpdateSystemConfigRequest", + "UpdateSystemConfigResponse", + "ValidateSystemConfigRequest", + "ConfigValidationIssue", + "ValidateSystemConfigResponse", + "SystemConfigValidationErrorResponse", + "SystemConfigConflictResponse", +] diff --git a/api/v1/schemas/analysis.py b/api/v1/schemas/analysis.py new file mode 100644 index 0000000..e4e906b --- /dev/null +++ b/api/v1/schemas/analysis.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +""" +=================================== +分析相关模型 +=================================== + +职责: +1. 定义分析请求和响应模型 +2. 定义任务状态模型 +3. 定义异步任务队列相关模型 +""" + +from typing import Optional, List, Any +from enum import Enum + +from pydantic import BaseModel, Field + + +class TaskStatusEnum(str, Enum): + """任务状态枚举""" + PENDING = "pending" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" + + +class AnalyzeRequest(BaseModel): + """分析请求模型""" + + stock_code: Optional[str] = Field( + None, + description="单只股票代码", + example="600519" + ) + stock_codes: Optional[List[str]] = Field( + None, + description="多只股票代码(与 stock_code 二选一)", + example=["600519", "000858"] + ) + report_type: str = Field( + "detailed", + description="报告类型", + pattern="^(simple|detailed)$" + ) + force_refresh: bool = Field( + True, + description="是否强制刷新(忽略缓存)" + ) + async_mode: bool = Field( + False, + description="是否使用异步模式" + ) + + class Config: + json_schema_extra = { + "example": { + "stock_code": "600519", + "report_type": "detailed", + "force_refresh": False, + "async_mode": False + } + } + + +class AnalysisResultResponse(BaseModel): + """分析结果响应模型""" + + query_id: str = Field(..., description="分析记录唯一标识") + stock_code: str = Field(..., description="股票代码") + stock_name: Optional[str] = Field(None, description="股票名称") + report: Optional[Any] = Field(None, description="分析报告") + created_at: str = Field(..., description="创建时间") + + class Config: + json_schema_extra = { + "example": { + "query_id": "abc123def456", + "stock_code": "600519", + "stock_name": "贵州茅台", + "report": { + "summary": { + "sentiment_score": 75, + "operation_advice": "持有" + } + }, + "created_at": "2024-01-01T12:00:00" + } + } + + +class TaskAccepted(BaseModel): + """异步任务接受响应""" + + task_id: str = Field(..., description="任务 ID,用于查询状态") + status: str = Field( + ..., + description="任务状态", + pattern="^(pending|processing)$" + ) + message: Optional[str] = Field(None, description="提示信息") + + class Config: + json_schema_extra = { + "example": { + "task_id": "task_abc123", + "status": "pending", + "message": "Analysis task accepted" + } + } + + +class TaskStatus(BaseModel): + """任务状态模型""" + + task_id: str = Field(..., description="任务 ID") + status: str = Field( + ..., + description="任务状态", + pattern="^(pending|processing|completed|failed)$" + ) + progress: Optional[int] = Field( + None, + description="进度百分比 (0-100)", + ge=0, + le=100 + ) + result: Optional[AnalysisResultResponse] = Field( + None, + description="分析结果(仅在 completed 时存在)" + ) + error: Optional[str] = Field( + None, + description="错误信息(仅在 failed 时存在)" + ) + + class Config: + json_schema_extra = { + "example": { + "task_id": "task_abc123", + "status": "completed", + "progress": 100, + "result": None, + "error": None + } + } + + +class TaskInfo(BaseModel): + """ + 任务详情模型 + + 用于任务列表和 SSE 事件推送 + """ + + task_id: str = Field(..., description="任务 ID") + stock_code: str = Field(..., description="股票代码") + stock_name: Optional[str] = Field(None, description="股票名称") + status: TaskStatusEnum = Field(..., description="任务状态") + progress: int = Field(0, description="进度百分比 (0-100)", ge=0, le=100) + message: Optional[str] = Field(None, description="状态消息") + report_type: str = Field("detailed", description="报告类型") + created_at: str = Field(..., description="创建时间") + started_at: Optional[str] = Field(None, description="开始执行时间") + completed_at: Optional[str] = Field(None, description="完成时间") + error: Optional[str] = Field(None, description="错误信息(仅在 failed 时存在)") + + class Config: + json_schema_extra = { + "example": { + "task_id": "abc123def456", + "stock_code": "600519", + "stock_name": "贵州茅台", + "status": "processing", + "progress": 50, + "message": "正在分析中...", + "report_type": "detailed", + "created_at": "2026-02-05T10:30:00", + "started_at": "2026-02-05T10:30:01", + "completed_at": None, + "error": None + } + } + + +class TaskListResponse(BaseModel): + """任务列表响应模型""" + + total: int = Field(..., description="任务总数") + pending: int = Field(..., description="等待中的任务数") + processing: int = Field(..., description="处理中的任务数") + tasks: List[TaskInfo] = Field(..., description="任务列表") + + class Config: + json_schema_extra = { + "example": { + "total": 3, + "pending": 1, + "processing": 2, + "tasks": [] + } + } + + +class DuplicateTaskErrorResponse(BaseModel): + """重复任务错误响应模型""" + + error: str = Field("duplicate_task", description="错误类型") + message: str = Field(..., description="错误信息") + stock_code: str = Field(..., description="股票代码") + existing_task_id: str = Field(..., description="已存在的任务 ID") + + class Config: + json_schema_extra = { + "example": { + "error": "duplicate_task", + "message": "股票 600519 正在分析中", + "stock_code": "600519", + "existing_task_id": "abc123def456" + } + } diff --git a/api/v1/schemas/backtest.py b/api/v1/schemas/backtest.py new file mode 100644 index 0000000..7d60c10 --- /dev/null +++ b/api/v1/schemas/backtest.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +"""Backtest API schemas.""" + +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + + +class BacktestRunRequest(BaseModel): + code: Optional[str] = Field(None, description="仅回测指定股票") + force: bool = Field(False, description="强制重新计算") + eval_window_days: Optional[int] = Field(None, ge=1, le=120, description="评估窗口(交易日数)") + min_age_days: Optional[int] = Field(None, ge=0, le=365, description="分析记录最小天龄(0=不限)") + limit: int = Field(200, ge=1, le=2000, description="最多处理的分析记录数") + + +class BacktestRunResponse(BaseModel): + processed: int = Field(..., description="候选记录数") + saved: int = Field(..., description="写入回测结果数") + completed: int = Field(..., description="完成回测数") + insufficient: int = Field(..., description="数据不足数") + errors: int = Field(..., description="错误数") + + +class BacktestResultItem(BaseModel): + analysis_history_id: int + code: str + analysis_date: Optional[str] = None + eval_window_days: int + engine_version: str + eval_status: str + evaluated_at: Optional[str] = None + operation_advice: Optional[str] = None + position_recommendation: Optional[str] = None + start_price: Optional[float] = None + end_close: Optional[float] = None + max_high: Optional[float] = None + min_low: Optional[float] = None + stock_return_pct: Optional[float] = None + direction_expected: Optional[str] = None + direction_correct: Optional[bool] = None + outcome: Optional[str] = None + stop_loss: Optional[float] = None + take_profit: Optional[float] = None + hit_stop_loss: Optional[bool] = None + hit_take_profit: Optional[bool] = None + first_hit: Optional[str] = None + first_hit_date: Optional[str] = None + first_hit_trading_days: Optional[int] = None + simulated_entry_price: Optional[float] = None + simulated_exit_price: Optional[float] = None + simulated_exit_reason: Optional[str] = None + simulated_return_pct: Optional[float] = None + + +class BacktestResultsResponse(BaseModel): + total: int + page: int + limit: int + items: List[BacktestResultItem] = Field(default_factory=list) + + +class PerformanceMetrics(BaseModel): + scope: str + code: Optional[str] = None + eval_window_days: int + engine_version: str + computed_at: Optional[str] = None + + total_evaluations: int + completed_count: int + insufficient_count: int + long_count: int + cash_count: int + win_count: int + loss_count: int + neutral_count: int + + direction_accuracy_pct: Optional[float] = None + win_rate_pct: Optional[float] = None + neutral_rate_pct: Optional[float] = None + avg_stock_return_pct: Optional[float] = None + avg_simulated_return_pct: Optional[float] = None + + stop_loss_trigger_rate: Optional[float] = None + take_profit_trigger_rate: Optional[float] = None + ambiguous_rate: Optional[float] = None + avg_days_to_first_hit: Optional[float] = None + + advice_breakdown: Dict[str, Any] = Field(default_factory=dict) + diagnostics: Dict[str, Any] = Field(default_factory=dict) + diff --git a/api/v1/schemas/common.py b/api/v1/schemas/common.py new file mode 100644 index 0000000..8b3fc90 --- /dev/null +++ b/api/v1/schemas/common.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +""" +=================================== +通用响应模型 +=================================== + +职责: +1. 定义通用的响应模型(HealthResponse, ErrorResponse 等) +2. 提供统一的响应格式 +""" + +from typing import Optional, Any + +from pydantic import BaseModel, Field + + +class RootResponse(BaseModel): + """API 根路由响应""" + + message: str = Field(..., description="API 运行状态消息", example="Daily Stock Analysis API is running") + version: Optional[str] = Field(None, description="API 版本", example="1.0.0") + + class Config: + json_schema_extra = { + "example": { + "message": "Daily Stock Analysis API is running", + "version": "1.0.0" + } + } + + +class HealthResponse(BaseModel): + """健康检查响应""" + + status: str = Field(..., description="服务状态", example="ok") + timestamp: Optional[str] = Field(None, description="时间戳") + + class Config: + json_schema_extra = { + "example": { + "status": "ok", + "timestamp": "2024-01-01T12:00:00" + } + } + + +class ErrorResponse(BaseModel): + """错误响应""" + + error: str = Field(..., description="错误类型", example="validation_error") + message: str = Field(..., description="错误详情", example="请求参数错误") + detail: Optional[Any] = Field(None, description="附加错误信息") + + class Config: + json_schema_extra = { + "example": { + "error": "not_found", + "message": "资源不存在", + "detail": None + } + } + + +class SuccessResponse(BaseModel): + """通用成功响应""" + + success: bool = Field(True, description="是否成功") + message: Optional[str] = Field(None, description="成功消息") + data: Optional[Any] = Field(None, description="响应数据") + + class Config: + json_schema_extra = { + "example": { + "success": True, + "message": "操作成功", + "data": None + } + } diff --git a/api/v1/schemas/history.py b/api/v1/schemas/history.py new file mode 100644 index 0000000..20d8495 --- /dev/null +++ b/api/v1/schemas/history.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +""" +=================================== +历史记录相关模型 +=================================== + +职责: +1. 定义历史记录列表和详情模型 +2. 定义分析报告完整模型 +""" + +from typing import Optional, List, Any + +from pydantic import BaseModel, Field + + +class HistoryItem(BaseModel): + """历史记录摘要(列表展示用)""" + + query_id: str = Field(..., description="分析记录唯一标识") + stock_code: str = Field(..., description="股票代码") + stock_name: Optional[str] = Field(None, description="股票名称") + report_type: Optional[str] = Field(None, description="报告类型") + sentiment_score: Optional[int] = Field( + None, + description="情绪评分 (0-100)", + ge=0, + le=100 + ) + operation_advice: Optional[str] = Field(None, description="操作建议") + created_at: Optional[str] = Field(None, description="创建时间") + + class Config: + json_schema_extra = { + "example": { + "query_id": "abc123", + "stock_code": "600519", + "stock_name": "贵州茅台", + "report_type": "detailed", + "sentiment_score": 75, + "operation_advice": "持有", + "created_at": "2024-01-01T12:00:00" + } + } + + +class HistoryListResponse(BaseModel): + """历史记录列表响应""" + + total: int = Field(..., description="总记录数") + page: int = Field(..., description="当前页码") + limit: int = Field(..., description="每页数量") + items: List[HistoryItem] = Field(default_factory=list, description="记录列表") + + class Config: + json_schema_extra = { + "example": { + "total": 100, + "page": 1, + "limit": 20, + "items": [] + } + } + + +class NewsIntelItem(BaseModel): + """新闻情报条目""" + + title: str = Field(..., description="新闻标题") + snippet: str = Field("", description="新闻摘要(最多200字)") + url: str = Field(..., description="新闻链接") + + class Config: + json_schema_extra = { + "example": { + "title": "公司发布业绩快报,营收同比增长 20%", + "snippet": "公司公告显示,季度营收同比增长 20%...", + "url": "https://example.com/news/123" + } + } + + +class NewsIntelResponse(BaseModel): + """新闻情报响应""" + + total: int = Field(..., description="新闻条数") + items: List[NewsIntelItem] = Field(default_factory=list, description="新闻列表") + + class Config: + json_schema_extra = { + "example": { + "total": 2, + "items": [] + } + } + + +class ReportMeta(BaseModel): + """报告元信息""" + + query_id: str = Field(..., description="分析记录唯一标识") + stock_code: str = Field(..., description="股票代码") + stock_name: Optional[str] = Field(None, description="股票名称") + report_type: Optional[str] = Field(None, description="报告类型") + created_at: Optional[str] = Field(None, description="创建时间") + current_price: Optional[float] = Field(None, description="分析时股价") + change_pct: Optional[float] = Field(None, description="分析时涨跌幅(%)") + + +class ReportSummary(BaseModel): + """报告概览区""" + + analysis_summary: Optional[str] = Field(None, description="关键结论") + operation_advice: Optional[str] = Field(None, description="操作建议") + trend_prediction: Optional[str] = Field(None, description="趋势预测") + sentiment_score: Optional[int] = Field( + None, + description="情绪评分 (0-100)", + ge=0, + le=100 + ) + sentiment_label: Optional[str] = Field(None, description="情绪标签") + + +class ReportStrategy(BaseModel): + """策略点位区""" + + ideal_buy: Optional[str] = Field(None, description="理想买入价") + secondary_buy: Optional[str] = Field(None, description="第二买入价") + stop_loss: Optional[str] = Field(None, description="止损价") + take_profit: Optional[str] = Field(None, description="止盈价") + + +class ReportDetails(BaseModel): + """报告详情区""" + + news_content: Optional[str] = Field(None, description="新闻摘要") + raw_result: Optional[Any] = Field(None, description="原始分析结果(JSON)") + context_snapshot: Optional[Any] = Field(None, description="分析时上下文快照(JSON)") + + +class AnalysisReport(BaseModel): + """完整分析报告""" + + meta: ReportMeta = Field(..., description="元信息") + summary: ReportSummary = Field(..., description="概览区") + strategy: Optional[ReportStrategy] = Field(None, description="策略点位区") + details: Optional[ReportDetails] = Field(None, description="详情区") + + class Config: + json_schema_extra = { + "example": { + "meta": { + "query_id": "abc123", + "stock_code": "600519", + "stock_name": "贵州茅台", + "report_type": "detailed", + "created_at": "2024-01-01T12:00:00" + }, + "summary": { + "analysis_summary": "技术面向好,建议持有", + "operation_advice": "持有", + "trend_prediction": "看多", + "sentiment_score": 75, + "sentiment_label": "乐观" + }, + "strategy": { + "ideal_buy": "1800.00", + "secondary_buy": "1750.00", + "stop_loss": "1700.00", + "take_profit": "2000.00" + }, + "details": None + } + } diff --git a/api/v1/schemas/stocks.py b/api/v1/schemas/stocks.py new file mode 100644 index 0000000..c375746 --- /dev/null +++ b/api/v1/schemas/stocks.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +""" +=================================== +股票数据相关模型 +=================================== + +职责: +1. 定义股票实时行情模型 +2. 定义历史 K 线数据模型 +""" + +from typing import Optional, List + +from pydantic import BaseModel, Field + + +class StockQuote(BaseModel): + """股票实时行情""" + + stock_code: str = Field(..., description="股票代码") + stock_name: Optional[str] = Field(None, description="股票名称") + current_price: float = Field(..., description="当前价格") + change: Optional[float] = Field(None, description="涨跌额") + change_percent: Optional[float] = Field(None, description="涨跌幅 (%)") + open: Optional[float] = Field(None, description="开盘价") + high: Optional[float] = Field(None, description="最高价") + low: Optional[float] = Field(None, description="最低价") + prev_close: Optional[float] = Field(None, description="昨收价") + volume: Optional[float] = Field(None, description="成交量(股)") + amount: Optional[float] = Field(None, description="成交额(元)") + update_time: Optional[str] = Field(None, description="更新时间") + + class Config: + json_schema_extra = { + "example": { + "stock_code": "600519", + "stock_name": "贵州茅台", + "current_price": 1800.00, + "change": 15.00, + "change_percent": 0.84, + "open": 1785.00, + "high": 1810.00, + "low": 1780.00, + "prev_close": 1785.00, + "volume": 10000000, + "amount": 18000000000, + "update_time": "2024-01-01T15:00:00" + } + } + + +class KLineData(BaseModel): + """K 线数据点""" + + date: str = Field(..., description="日期") + open: float = Field(..., description="开盘价") + high: float = Field(..., description="最高价") + low: float = Field(..., description="最低价") + close: float = Field(..., description="收盘价") + volume: Optional[float] = Field(None, description="成交量") + amount: Optional[float] = Field(None, description="成交额") + change_percent: Optional[float] = Field(None, description="涨跌幅 (%)") + + class Config: + json_schema_extra = { + "example": { + "date": "2024-01-01", + "open": 1785.00, + "high": 1810.00, + "low": 1780.00, + "close": 1800.00, + "volume": 10000000, + "amount": 18000000000, + "change_percent": 0.84 + } + } + + +class StockHistoryResponse(BaseModel): + """股票历史行情响应""" + + stock_code: str = Field(..., description="股票代码") + stock_name: Optional[str] = Field(None, description="股票名称") + period: str = Field(..., description="K 线周期") + data: List[KLineData] = Field(default_factory=list, description="K 线数据列表") + + class Config: + json_schema_extra = { + "example": { + "stock_code": "600519", + "stock_name": "贵州茅台", + "period": "daily", + "data": [] + } + } diff --git a/api/v1/schemas/system_config.py b/api/v1/schemas/system_config.py new file mode 100644 index 0000000..eb805b0 --- /dev/null +++ b/api/v1/schemas/system_config.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +"""System configuration API schemas.""" + +from __future__ import annotations + +from typing import Any, Dict, List, Literal, Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class SystemConfigFieldSchema(BaseModel): + """Metadata schema for a single config field.""" + + key: str = Field(..., description="Configuration key name") + title: Optional[str] = Field(None, description="Display title") + description: Optional[str] = Field(None, description="Field description") + category: Literal["base", "data_source", "ai_model", "notification", "system", "backtest", "uncategorized"] + data_type: Literal["string", "integer", "number", "boolean", "array", "json", "time"] + ui_control: Literal["text", "password", "number", "select", "textarea", "switch", "time"] + is_sensitive: bool + is_required: bool + is_editable: bool + default_value: Optional[str] = None + options: List[str] = Field(default_factory=list) + validation: Dict[str, Any] = Field(default_factory=dict) + display_order: int + + +class SystemConfigCategorySchema(BaseModel): + """Category grouping metadata.""" + + category: str + title: str + description: Optional[str] = None + display_order: int + fields: List[SystemConfigFieldSchema] + + +class SystemConfigSchemaResponse(BaseModel): + """Metadata response for dynamic frontend rendering.""" + + schema_version: str + categories: List[SystemConfigCategorySchema] + + +class SystemConfigItem(BaseModel): + """Config value entry with optional schema metadata.""" + + model_config = ConfigDict(populate_by_name=True) + + key: str + value: str + raw_value_exists: bool + is_masked: bool + schema_: Optional[SystemConfigFieldSchema] = Field(default=None, alias="schema") + + +class SystemConfigResponse(BaseModel): + """Read response for current configuration values.""" + + config_version: str + mask_token: str + items: List[SystemConfigItem] + updated_at: Optional[str] = None + + +class SystemConfigUpdateItem(BaseModel): + """Single key-value update item.""" + + key: str + value: str + + +class UpdateSystemConfigRequest(BaseModel): + """Update request payload.""" + + config_version: str + mask_token: str = "******" + reload_now: bool = True + items: List[SystemConfigUpdateItem] = Field(..., min_length=1) + + +class UpdateSystemConfigResponse(BaseModel): + """Update operation result payload.""" + + success: bool + config_version: str + applied_count: int + skipped_masked_count: int + reload_triggered: bool + updated_keys: List[str] + warnings: List[str] = Field(default_factory=list) + + +class ValidateSystemConfigRequest(BaseModel): + """Validation request payload.""" + + items: List[SystemConfigUpdateItem] = Field(..., min_length=1) + + +class ConfigValidationIssue(BaseModel): + """Validation issue details.""" + + key: str + code: str + message: str + severity: Literal["error", "warning"] + expected: Optional[str] = None + actual: Optional[str] = None + + +class ValidateSystemConfigResponse(BaseModel): + """Validation result payload.""" + + valid: bool + issues: List[ConfigValidationIssue] + + +class SystemConfigValidationErrorResponse(BaseModel): + """Error payload for failed update validation.""" + + error: str + message: str + issues: List[ConfigValidationIssue] + + +class SystemConfigConflictResponse(BaseModel): + """Error payload for optimistic lock conflict.""" + + error: str + message: str + current_config_version: str diff --git a/apps/dsa-desktop/main.js b/apps/dsa-desktop/main.js new file mode 100644 index 0000000..e49756d --- /dev/null +++ b/apps/dsa-desktop/main.js @@ -0,0 +1,558 @@ +const { app, BrowserWindow, shell } = require('electron'); +const path = require('path'); +const fs = require('fs'); +const { spawn } = require('child_process'); +const net = require('net'); +const http = require('http'); + +let mainWindow = null; +let backendProcess = null; +let logFilePath = null; +let backendStartError = null; + +const isWindows = process.platform === 'win32'; +const appRootDev = path.resolve(__dirname, '..', '..'); + +function resolveEnvExamplePath() { + if (app.isPackaged) { + return path.join(process.resourcesPath, '.env.example'); + } + return path.join(appRootDev, '.env.example'); +} + +function resolveAppDir() { + if (app.isPackaged) { + // exe 所在目录 + return path.dirname(app.getPath('exe')); + } + return app.getPath('userData'); +} + +function resolveBackendPath() { + if (process.env.DSA_BACKEND_PATH) { + return process.env.DSA_BACKEND_PATH; + } + + if (app.isPackaged) { + const backendDir = path.join(process.resourcesPath, 'backend'); + const exeName = isWindows ? 'stock_analysis.exe' : 'stock_analysis'; + const oneDirPath = path.join(backendDir, 'stock_analysis', exeName); + if (fs.existsSync(oneDirPath)) { + return oneDirPath; + } + return path.join(backendDir, exeName); + } + + return null; +} + +function initLogging() { + const appDir = app.isPackaged ? path.dirname(app.getPath('exe')) : app.getPath('userData'); + logFilePath = path.join(appDir, 'logs', 'desktop.log'); + + // 确保日志目录存在 + const logDir = path.dirname(logFilePath); + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }); + } + + logLine('Desktop app starting'); +} + +function logLine(message) { + const timestamp = new Date().toISOString(); + const line = `[${timestamp}] ${message}\n`; + try { + if (logFilePath) { + fs.appendFileSync(logFilePath, line, 'utf-8'); + } + } catch (error) { + console.error(error); + } + console.log(line.trim()); +} + +function formatCommand(command, args = []) { + return [command, ...args] + .map((part) => { + const value = String(part); + return value.includes(' ') ? `"${value}"` : value; + }) + .join(' '); +} + +function resolvePythonPath() { + return process.env.DSA_PYTHON || 'python'; +} + +function ensureEnvFile(envPath) { + if (fs.existsSync(envPath)) { + return; + } + + const envExample = resolveEnvExamplePath(); + if (fs.existsSync(envExample)) { + fs.copyFileSync(envExample, envPath); + return; + } + + fs.writeFileSync(envPath, '# Configure your API keys and stock list here.\n', 'utf-8'); +} + +function findAvailablePort(startPort = 8000, endPort = 8100) { + return new Promise((resolve, reject) => { + const tryPort = (port) => { + if (port > endPort) { + reject(new Error('No available port')); + return; + } + + const server = net.createServer(); + server.once('error', () => { + tryPort(port + 1); + }); + server.once('listening', () => { + server.close(() => resolve(port)); + }); + server.listen(port, '127.0.0.1'); + }; + + tryPort(startPort); + }); +} + +function waitForHealth( + url, + timeoutMs = 60000, + intervalMs = 250, + requestTimeoutMs = 1500, + shouldAbort = null, + onProgress = null +) { + const start = Date.now(); + let attempts = 0; + + return new Promise((resolve, reject) => { + let settled = false; + let retryTimer = null; + let activeRequest = null; + + const emitProgress = (payload) => { + if (typeof onProgress !== 'function') { + return; + } + try { + onProgress(payload); + } catch (_error) { + } + }; + + const finish = (error, result) => { + if (settled) { + return; + } + settled = true; + + if (retryTimer) { + clearTimeout(retryTimer); + retryTimer = null; + } + + if (activeRequest && !activeRequest.destroyed) { + activeRequest.destroy(); + } + + if (error) { + emitProgress({ + type: 'final_error', + elapsedMs: Date.now() - start, + attempts, + message: error.message, + }); + } + + if (error) { + reject(error); + } else { + resolve(result); + } + }; + + const scheduleNext = () => { + if (settled) { + return; + } + retryTimer = setTimeout(attempt, intervalMs); + }; + + const attempt = () => { + if (settled) { + return; + } + + if (typeof shouldAbort === 'function') { + const abortReason = shouldAbort(); + if (abortReason) { + emitProgress({ + type: 'aborted', + elapsedMs: Date.now() - start, + attempts, + reason: abortReason, + }); + finish(new Error(`Health check aborted: ${abortReason}`)); + return; + } + } + + const elapsedMs = Date.now() - start; + if (elapsedMs > timeoutMs) { + emitProgress({ + type: 'total_timeout', + elapsedMs, + attempts, + timeoutMs, + }); + finish(new Error(`Health check timeout after ${elapsedMs}ms`)); + return; + } + + attempts += 1; + emitProgress({ + type: 'probe_start', + elapsedMs, + attempts, + }); + + activeRequest = http.get(url, (res) => { + if (settled) { + return; + } + + res.resume(); + if (res.statusCode === 200) { + const readyElapsedMs = Date.now() - start; + emitProgress({ + type: 'ready', + elapsedMs: readyElapsedMs, + attempts, + }); + finish(null, { elapsedMs: readyElapsedMs, attempts }); + return; + } + + emitProgress({ + type: 'probe_status', + elapsedMs: Date.now() - start, + attempts, + statusCode: res.statusCode, + }); + scheduleNext(); + }); + + activeRequest.setTimeout(requestTimeoutMs, () => { + emitProgress({ + type: 'probe_timeout', + elapsedMs: Date.now() - start, + attempts, + requestTimeoutMs, + }); + activeRequest.destroy(new Error(`Health probe request timeout after ${requestTimeoutMs}ms`)); + }); + + activeRequest.on('error', (error) => { + if (settled) { + return; + } + + emitProgress({ + type: 'probe_error', + elapsedMs: Date.now() - start, + attempts, + errorCode: error.code || 'unknown', + errorMessage: error.message, + }); + scheduleNext(); + }); + }; + + attempt(); + }); +} + +function startBackend({ port, envFile, dbPath, logDir }) { + const backendPath = resolveBackendPath(); + backendStartError = null; + const launchStartedAt = Date.now(); + + const env = { + ...process.env, + DSA_DESKTOP_MODE: 'true', + ENV_FILE: envFile, + DATABASE_PATH: dbPath, + LOG_DIR: logDir, + PYTHONUTF8: '1', + SCHEDULE_ENABLED: 'false', + WEBUI_ENABLED: 'false', + BOT_ENABLED: 'false', + DINGTALK_STREAM_ENABLED: 'false', + FEISHU_STREAM_ENABLED: 'false', + }; + + const args = ['--serve-only', '--host', '127.0.0.1', '--port', String(port)]; + let launchMode = ''; + let launchCommand = ''; + let launchCwd = ''; + + if (backendPath) { + if (!fs.existsSync(backendPath)) { + throw new Error(`Backend executable not found: ${backendPath}`); + } + launchMode = 'packaged'; + launchCommand = formatCommand(backendPath, args); + launchCwd = path.dirname(backendPath); + backendProcess = spawn(backendPath, args, { + env, + cwd: launchCwd, + stdio: 'pipe', + windowsHide: true, + }); + } else { + const pythonPath = resolvePythonPath(); + const scriptPath = path.join(appRootDev, 'main.py'); + launchMode = 'development'; + launchCommand = formatCommand(pythonPath, [scriptPath, ...args]); + launchCwd = appRootDev; + backendProcess = spawn(pythonPath, [scriptPath, ...args], { + env, + cwd: launchCwd, + stdio: 'pipe', + windowsHide: true, + }); + } + + if (backendProcess) { + let firstStdoutLogged = false; + let firstStderrLogged = false; + + backendProcess.once('spawn', () => { + logLine(`[backend] spawned pid=${backendProcess.pid} in ${Date.now() - launchStartedAt}ms`); + }); + backendProcess.on('error', (error) => { + backendStartError = error; + logLine(`[backend] failed to start: ${error.message}`); + }); + backendProcess.stdout.on('data', (data) => { + if (!firstStdoutLogged) { + firstStdoutLogged = true; + logLine(`[backend] first stdout after ${Date.now() - launchStartedAt}ms`); + } + logLine(`[backend] ${String(data).trim()}`); + }); + backendProcess.stderr.on('data', (data) => { + if (!firstStderrLogged) { + firstStderrLogged = true; + logLine(`[backend] first stderr after ${Date.now() - launchStartedAt}ms`); + } + logLine(`[backend] ${String(data).trim()}`); + }); + backendProcess.on('exit', (code, signal) => { + logLine(`[backend] exited with code ${code}, signal ${signal || 'none'}`); + }); + } + + return { + mode: launchMode, + command: launchCommand, + cwd: launchCwd, + }; +} + +function stopBackend() { + if (!backendProcess || backendProcess.killed) { + return; + } + + if (isWindows) { + spawn('taskkill', ['/PID', String(backendProcess.pid), '/T', '/F']); + return; + } + + backendProcess.kill('SIGTERM'); + setTimeout(() => { + if (!backendProcess.killed) { + backendProcess.kill('SIGKILL'); + } + }, 3000); +} + +async function createWindow() { + initLogging(); + const startupStartedAt = Date.now(); + const logStartup = (message) => { + logLine(`[startup +${Date.now() - startupStartedAt}ms] ${message}`); + }; + + logStartup('createWindow started'); + + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + minWidth: 960, + minHeight: 640, + backgroundColor: '#0f172a', + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + nodeIntegration: false, + contextIsolation: true, + }, + }); + logStartup('BrowserWindow created'); + + const loadingPath = path.join(__dirname, 'renderer', 'loading.html'); + const loadingPageStartedAt = Date.now(); + await mainWindow.loadFile(loadingPath); + logStartup(`Loading page rendered in ${Date.now() - loadingPageStartedAt}ms`); + + const webViewStartedAt = Date.now(); + mainWindow.webContents.on('did-start-loading', () => { + logStartup('WebContents did-start-loading'); + }); + mainWindow.webContents.on('dom-ready', () => { + logStartup(`WebContents dom-ready (+${Date.now() - webViewStartedAt}ms after events attached)`); + }); + mainWindow.webContents.on('did-finish-load', () => { + logStartup(`WebContents did-finish-load (+${Date.now() - webViewStartedAt}ms after events attached)`); + }); + mainWindow.webContents.on( + 'did-fail-load', + (_event, errorCode, errorDescription, validatedURL, isMainFrame) => { + logStartup( + `WebContents did-fail-load code=${errorCode} mainFrame=${isMainFrame} url=${validatedURL} reason=${errorDescription}` + ); + } + ); + + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + shell.openExternal(url); + return { action: 'deny' }; + }); + + const appDir = resolveAppDir(); + const envPath = path.join(appDir, '.env'); + ensureEnvFile(envPath); + logStartup(`Env file ready: ${envPath}`); + + const portFindStartedAt = Date.now(); + const port = await findAvailablePort(8000, 8100); + logStartup(`Using port ${port} (selected in ${Date.now() - portFindStartedAt}ms)`); + logStartup(`App directory=${appDir}`); + + const dbPath = path.join(appDir, 'data', 'stock_analysis.db'); + const logDir = path.join(appDir, 'logs'); + + try { + const launchInfo = startBackend({ port, envFile: envPath, dbPath, logDir }); + logStartup(`Backend launch mode=${launchInfo.mode}`); + logStartup(`Backend launch command=${launchInfo.command}`); + logStartup(`Backend launch cwd=${launchInfo.cwd}`); + logStartup('Waiting for backend health check'); + } catch (error) { + logStartup(`Backend launch failed: ${String(error)}`); + const errorUrl = `file://${loadingPath}?error=${encodeURIComponent(String(error))}`; + await mainWindow.loadURL(errorUrl); + return; + } + + const healthUrl = `http://127.0.0.1:${port}/api/health`; + let lastHealthProgressLogAt = 0; + const healthProgressLogIntervalMs = 2000; + + const onHealthProgress = (event) => { + if (!event || event.type === 'probe_start') { + return; + } + + if (event.type === 'ready') { + logStartup(`Health ready in ${event.elapsedMs}ms (attempts=${event.attempts})`); + return; + } + + if (event.type === 'aborted' || event.type === 'total_timeout' || event.type === 'final_error') { + const details = event.reason || event.message || ''; + logStartup(`Health ${event.type} after ${event.elapsedMs}ms (attempts=${event.attempts}) ${details}`.trim()); + return; + } + + const now = Date.now(); + if (now - lastHealthProgressLogAt < healthProgressLogIntervalMs) { + return; + } + + lastHealthProgressLogAt = now; + let detail = ''; + if (event.type === 'probe_status') { + detail = `status=${event.statusCode}`; + } else if (event.type === 'probe_timeout') { + detail = `probeTimeout=${event.requestTimeoutMs}ms`; + } else if (event.type === 'probe_error') { + detail = `error=${event.errorCode}:${event.errorMessage}`; + } + + logStartup( + `Waiting for backend health... elapsed=${event.elapsedMs}ms attempts=${event.attempts}${detail ? ` ${detail}` : ''}` + ); + }; + + try { + const healthInfo = await waitForHealth( + healthUrl, + 60000, + 250, + 1500, + () => { + if (backendStartError) { + return `backend start error: ${backendStartError.message}`; + } + if (!backendProcess) { + return 'backend process is unavailable'; + } + if (backendProcess.exitCode !== null) { + return `backend exited with code ${backendProcess.exitCode}`; + } + if (backendProcess.signalCode) { + return `backend exited by signal ${backendProcess.signalCode}`; + } + return null; + }, + onHealthProgress + ); + logStartup(`Backend ready in ${healthInfo.elapsedMs}ms (${healthInfo.attempts} probes)`); + const mainPageStartedAt = Date.now(); + await mainWindow.loadURL(`http://127.0.0.1:${port}/`); + logStartup(`Main page loadURL resolved in ${Date.now() - mainPageStartedAt}ms`); + logStartup(`Main UI loaded in ${Date.now() - startupStartedAt}ms`); + } catch (error) { + logStartup(`Startup failed while waiting for health: ${String(error)}`); + const errorUrl = `file://${loadingPath}?error=${encodeURIComponent(String(error))}`; + await mainWindow.loadURL(errorUrl); + } +} + +app.whenReady().then(createWindow); + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); + +app.on('window-all-closed', () => { + stopBackend(); + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('before-quit', () => { + stopBackend(); +}); diff --git a/apps/dsa-desktop/package.json b/apps/dsa-desktop/package.json new file mode 100644 index 0000000..91e9cd5 --- /dev/null +++ b/apps/dsa-desktop/package.json @@ -0,0 +1,42 @@ +{ + "name": "daily-stock-analysis-desktop", + "version": "0.1.0", + "private": true, + "main": "main.js", + "scripts": { + "dev": "electron .", + "build": "electron-builder" + }, + "devDependencies": { + "electron": "^31.4.0", + "electron-builder": "^24.13.3" + }, + "build": { + "appId": "com.daily-stock-analysis.desktop", + "productName": "Daily Stock Analysis", + "directories": { + "output": "dist" + }, + "files": [ + "main.js", + "preload.js", + "renderer/**/*" + ], + "extraResources": [ + { + "from": "../../.env.example", + "to": ".env.example" + }, + { + "from": "../../dist/backend/stock_analysis", + "to": "backend/stock_analysis" + } + ], + "win": { + "target": "portable" + }, + "mac": { + "target": "dmg" + } + } +} diff --git a/apps/dsa-desktop/preload.js b/apps/dsa-desktop/preload.js new file mode 100644 index 0000000..9539e84 --- /dev/null +++ b/apps/dsa-desktop/preload.js @@ -0,0 +1,5 @@ +const { contextBridge } = require('electron'); + +contextBridge.exposeInMainWorld('dsaDesktop', { + version: '0.1.0', +}); diff --git a/apps/dsa-desktop/renderer/loading.html b/apps/dsa-desktop/renderer/loading.html new file mode 100644 index 0000000..381735d --- /dev/null +++ b/apps/dsa-desktop/renderer/loading.html @@ -0,0 +1,122 @@ + + + + + + Daily Stock Analysis + + + +
+

Daily Stock Analysis

+

Launching local analysis service

+
+ + Preparing backend and loading UI... +
+

If this takes long, check your API keys and network connectivity.

+
+
+ + + diff --git a/apps/dsa-web/.gitignore b/apps/dsa-web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/apps/dsa-web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/dsa-web/eslint.config.js b/apps/dsa-web/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/apps/dsa-web/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/apps/dsa-web/index.html b/apps/dsa-web/index.html new file mode 100644 index 0000000..2eed617 --- /dev/null +++ b/apps/dsa-web/index.html @@ -0,0 +1,13 @@ + + + + + + + dsa-web + + +
+ + + diff --git a/apps/dsa-web/package-lock.json b/apps/dsa-web/package-lock.json new file mode 100644 index 0000000..dd788b4 --- /dev/null +++ b/apps/dsa-web/package-lock.json @@ -0,0 +1,4397 @@ +{ + "name": "dsa-web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dsa-web", + "version": "0.0.0", + "dependencies": { + "axios": "^1.13.4", + "camelcase-keys": "^10.0.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/postcss": "^4.1.18", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.24", + "babel-plugin-react-compiler": "^1.0.0", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "postcss": "^8.4.41", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.10.tgz", + "integrity": "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", + "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/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/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.3.tgz", + "integrity": "sha512-NVUnA6gQCl8jfoYqKqQU5Clv0aPw14KkZYCsX6T9Lfu9slI0LOU10OTwFHS/WmptsMMpshNd/1tuWsHQ2Uk+cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.2", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/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/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-9.0.0.tgz", + "integrity": "sha512-TO9xmyXTZ9HUHI8M1OnvExxYB0eYVS/1e5s7IDMTAoIcwUd+aNcFODs6Xk83mobk0velyHFQgA1yIrvYc6wclw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-keys": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-10.0.2.tgz", + "integrity": "sha512-PVHCLVbJ7nWGal0lPAmBN5eSLjIynlMUk2EPmL9aPl6QyJ6+FoszTKwldPzkuVqg5teZbPTbb8Oenzyw9GSJRw==", + "license": "MIT", + "dependencies": { + "camelcase": "^9.0.0", + "map-obj": "6.0.0", + "quick-lru": "^7.3.0", + "type-fest": "^5.4.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001767", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", + "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/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/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "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.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/map-obj": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-6.0.0.tgz", + "integrity": "sha512-PwDvwt/tK70+luLw5k9ySLtzLAzwf7tZTY9GBj63Y010nHRPjwHcQTpTd5JwQqITC2ty7prtxBo71iwyYY0TAg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz", + "integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.3.tgz", + "integrity": "sha512-AXSAQJu79WGc79/3e9/CR77I/KQgeY1AhNvcShIH4PTcGYyC4xv6H4R4AUOwkPS5799KlVDAu8zExeCrkGquiA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/apps/dsa-web/package.json b/apps/dsa-web/package.json new file mode 100644 index 0000000..465ffe3 --- /dev/null +++ b/apps/dsa-web/package.json @@ -0,0 +1,39 @@ +{ + "name": "dsa-web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.13.4", + "camelcase-keys": "^10.0.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@tailwindcss/postcss": "^4.1.18", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "autoprefixer": "^10.4.24", + "babel-plugin-react-compiler": "^1.0.0", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.18", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/apps/dsa-web/postcss.config.js b/apps/dsa-web/postcss.config.js new file mode 100644 index 0000000..14502dc --- /dev/null +++ b/apps/dsa-web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + autoprefixer: {}, + }, +} diff --git a/apps/dsa-web/public/vite.svg b/apps/dsa-web/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/apps/dsa-web/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/dsa-web/src/App.css b/apps/dsa-web/src/App.css new file mode 100644 index 0000000..3d9d6df --- /dev/null +++ b/apps/dsa-web/src/App.css @@ -0,0 +1,4 @@ +#root { + width: 100%; + min-height: 100vh; +} diff --git a/apps/dsa-web/src/App.tsx b/apps/dsa-web/src/App.tsx new file mode 100644 index 0000000..d5085f9 --- /dev/null +++ b/apps/dsa-web/src/App.tsx @@ -0,0 +1,117 @@ +import type React from 'react'; +import {BrowserRouter as Router, Routes, Route, NavLink} from 'react-router-dom'; +import HomePage from './pages/HomePage'; +import BacktestPage from './pages/BacktestPage'; +import SettingsPage from './pages/SettingsPage'; +import NotFoundPage from './pages/NotFoundPage'; +import './App.css'; + +// 侧边导航图标 +const HomeIcon: React.FC<{ active?: boolean }> = ({active}) => ( + + + +); + +const BacktestIcon: React.FC<{ active?: boolean }> = ({active}) => ( + + + +); + +const SettingsIcon: React.FC<{ active?: boolean }> = ({active}) => ( + + + + +); + +type DockItem = { + key: string; + label: string; + to: string; + icon: React.FC<{ active?: boolean }>; +}; + +const NAV_ITEMS: DockItem[] = [ + { + key: 'home', + label: '首页', + to: '/', + icon: HomeIcon, + }, + { + key: 'backtest', + label: '回测', + to: '/backtest', + icon: BacktestIcon, + }, + { + key: 'settings', + label: '设置', + to: '/settings', + icon: SettingsIcon, + }, +]; + +// Dock 导航栏 +const DockNav: React.FC = () => { + return ( + + ); +}; + +const App: React.FC = () => { + return ( + +
+ {/* Dock 导航 */} + + + {/* 主内容区 */} +
+ + }/> + }/> + }/> + }/> + +
+
+
+ ); +}; + +export default App; diff --git a/apps/dsa-web/src/api/analysis.ts b/apps/dsa-web/src/api/analysis.ts new file mode 100644 index 0000000..038d4d8 --- /dev/null +++ b/apps/dsa-web/src/api/analysis.ts @@ -0,0 +1,146 @@ +import apiClient from './index'; +import { toCamelCase } from './utils'; +import type { + AnalysisRequest, + AnalysisResult, + AnalysisReport, + TaskStatus, + TaskListResponse, +} from '../types/analysis'; + +// ============ API 接口 ============ + +export const analysisApi = { + /** + * 触发股票分析 + * @param data 分析请求参数 + * @returns 同步模式返回 AnalysisResult,异步模式返回 TaskAccepted(需检查 status code) + */ + analyze: async (data: AnalysisRequest): Promise => { + const requestData = { + stock_code: data.stockCode, + report_type: data.reportType || 'detailed', + force_refresh: data.forceRefresh || false, + async_mode: data.asyncMode || false, + }; + + const response = await apiClient.post>( + '/api/v1/analysis/analyze', + requestData + ); + + const result = toCamelCase(response.data); + + // 确保 report 字段正确转换 + if (result.report) { + result.report = toCamelCase(result.report); + } + + return result; + }, + + /** + * 异步模式触发分析 + * 返回 task_id,通过 SSE 或轮询获取结果 + * @param data 分析请求参数 + * @returns 任务接受响应或抛出 409 错误 + */ + analyzeAsync: async (data: AnalysisRequest): Promise<{ taskId: string; status: string; message?: string }> => { + const requestData = { + stock_code: data.stockCode, + report_type: data.reportType || 'detailed', + force_refresh: data.forceRefresh || false, + async_mode: true, + }; + + const response = await apiClient.post>( + '/api/v1/analysis/analyze', + requestData, + { + // 允许 202 状态码 + validateStatus: (status) => status === 200 || status === 202 || status === 409, + } + ); + + // 处理 409 重复提交错误 + if (response.status === 409) { + const errorData = toCamelCase<{ + error: string; + message: string; + stockCode: string; + existingTaskId: string; + }>(response.data); + throw new DuplicateTaskError(errorData.stockCode, errorData.existingTaskId, errorData.message); + } + + return toCamelCase<{ taskId: string; status: string; message?: string }>(response.data); + }, + + /** + * 获取异步任务状态 + * @param taskId 任务 ID + */ + getStatus: async (taskId: string): Promise => { + const response = await apiClient.get>( + `/api/v1/analysis/status/${taskId}` + ); + + const data = toCamelCase(response.data); + + // 确保嵌套的 result 也被正确转换 + if (data.result) { + data.result = toCamelCase(data.result); + if (data.result.report) { + data.result.report = toCamelCase(data.result.report); + } + } + + return data; + }, + + /** + * 获取任务列表 + * @param params 筛选参数 + */ + getTasks: async (params?: { + status?: string; + limit?: number; + }): Promise => { + const response = await apiClient.get>( + '/api/v1/analysis/tasks', + { params } + ); + + const data = toCamelCase(response.data); + + return data; + }, + + /** + * 获取 SSE 流 URL + * 用于 EventSource 连接 + */ + getTaskStreamUrl: (): string => { + // 获取 API base URL + const baseUrl = apiClient.defaults.baseURL || ''; + return `${baseUrl}/api/v1/analysis/tasks/stream`; + }, +}; + +// ============ 自定义错误类 ============ + +/** + * 重复任务错误 + * 当股票正在分析中时抛出 + */ +export class DuplicateTaskError extends Error { + stockCode: string; + existingTaskId: string; + + constructor(stockCode: string, existingTaskId: string, message?: string) { + super(message || `股票 ${stockCode} 正在分析中`); + this.name = 'DuplicateTaskError'; + this.stockCode = stockCode; + this.existingTaskId = existingTaskId; + } +} diff --git a/apps/dsa-web/src/api/backtest.ts b/apps/dsa-web/src/api/backtest.ts new file mode 100644 index 0000000..15cc9d9 --- /dev/null +++ b/apps/dsa-web/src/api/backtest.ts @@ -0,0 +1,102 @@ +import apiClient from './index'; +import { toCamelCase } from './utils'; +import type { + BacktestRunRequest, + BacktestRunResponse, + BacktestResultsResponse, + BacktestResultItem, + PerformanceMetrics, +} from '../types/backtest'; + +// ============ API ============ + +export const backtestApi = { + /** + * Trigger backtest evaluation + */ + run: async (params: BacktestRunRequest = {}): Promise => { + const requestData: Record = {}; + if (params.code) requestData.code = params.code; + if (params.force) requestData.force = params.force; + if (params.evalWindowDays) requestData.eval_window_days = params.evalWindowDays; + if (params.minAgeDays != null) requestData.min_age_days = params.minAgeDays; + if (params.limit) requestData.limit = params.limit; + + const response = await apiClient.post>( + '/api/v1/backtest/run', + requestData, + ); + return toCamelCase(response.data); + }, + + /** + * Get paginated backtest results + */ + getResults: async (params: { + code?: string; + evalWindowDays?: number; + page?: number; + limit?: number; + } = {}): Promise => { + const { code, evalWindowDays, page = 1, limit = 20 } = params; + + const queryParams: Record = { page, limit }; + if (code) queryParams.code = code; + if (evalWindowDays) queryParams.eval_window_days = evalWindowDays; + + const response = await apiClient.get>( + '/api/v1/backtest/results', + { params: queryParams }, + ); + + const data = toCamelCase(response.data); + return { + total: data.total, + page: data.page, + limit: data.limit, + items: (data.items || []).map(item => toCamelCase(item)), + }; + }, + + /** + * Get overall performance metrics + */ + getOverallPerformance: async (evalWindowDays?: number): Promise => { + try { + const params: Record = {}; + if (evalWindowDays) params.eval_window_days = evalWindowDays; + const response = await apiClient.get>( + '/api/v1/backtest/performance', + { params }, + ); + return toCamelCase(response.data); + } catch (err: unknown) { + if (err && typeof err === 'object' && 'response' in err) { + const axiosErr = err as { response?: { status?: number } }; + if (axiosErr.response?.status === 404) return null; + } + throw err; + } + }, + + /** + * Get per-stock performance metrics + */ + getStockPerformance: async (code: string, evalWindowDays?: number): Promise => { + try { + const params: Record = {}; + if (evalWindowDays) params.eval_window_days = evalWindowDays; + const response = await apiClient.get>( + `/api/v1/backtest/performance/${encodeURIComponent(code)}`, + { params }, + ); + return toCamelCase(response.data); + } catch (err: unknown) { + if (err && typeof err === 'object' && 'response' in err) { + const axiosErr = err as { response?: { status?: number } }; + if (axiosErr.response?.status === 404) return null; + } + throw err; + } + }, +}; diff --git a/apps/dsa-web/src/api/history.ts b/apps/dsa-web/src/api/history.ts new file mode 100644 index 0000000..18f0861 --- /dev/null +++ b/apps/dsa-web/src/api/history.ts @@ -0,0 +1,70 @@ +import apiClient from './index'; +import { toCamelCase } from './utils'; +import type { + HistoryListResponse, + HistoryItem, + HistoryFilters, + AnalysisReport, + NewsIntelResponse, + NewsIntelItem, +} from '../types/analysis'; + +// ============ API 接口 ============ + +export interface GetHistoryListParams extends HistoryFilters { + page?: number; + limit?: number; +} + +export const historyApi = { + /** + * 获取历史分析列表 + * @param params 筛选和分页参数 + */ + getList: async (params: GetHistoryListParams = {}): Promise => { + const { stockCode, startDate, endDate, page = 1, limit = 20 } = params; + + const queryParams: Record = { page, limit }; + if (stockCode) queryParams.stock_code = stockCode; + if (startDate) queryParams.start_date = startDate; + if (endDate) queryParams.end_date = endDate; + + const response = await apiClient.get>('/api/v1/history', { + params: queryParams, + }); + + const data = toCamelCase<{ total: number; page: number; limit: number; items: HistoryItem[] }>(response.data); + return { + total: data.total, + page: data.page, + limit: data.limit, + items: data.items.map(item => toCamelCase(item)), + }; + }, + + /** + * 获取历史报告详情 + * @param queryId 分析记录唯一标识 + */ + getDetail: async (queryId: string): Promise => { + const response = await apiClient.get>(`/api/v1/history/${queryId}`); + return toCamelCase(response.data); + }, + + /** + * 获取历史报告关联新闻 + * @param queryId 分析记录唯一标识 + * @param limit 返回数量限制 + */ + getNews: async (queryId: string, limit = 20): Promise => { + const response = await apiClient.get>(`/api/v1/history/${queryId}/news`, { + params: { limit }, + }); + + const data = toCamelCase(response.data); + return { + total: data.total, + items: (data.items || []).map(item => toCamelCase(item)), + }; + }, +}; diff --git a/apps/dsa-web/src/api/index.ts b/apps/dsa-web/src/api/index.ts new file mode 100644 index 0000000..fb2c85d --- /dev/null +++ b/apps/dsa-web/src/api/index.ts @@ -0,0 +1,12 @@ +import axios from 'axios'; +import { API_BASE_URL } from '../utils/constants'; + +const apiClient = axios.create({ + baseURL: API_BASE_URL, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +export default apiClient; diff --git a/apps/dsa-web/src/api/systemConfig.ts b/apps/dsa-web/src/api/systemConfig.ts new file mode 100644 index 0000000..72d1a1e --- /dev/null +++ b/apps/dsa-web/src/api/systemConfig.ts @@ -0,0 +1,124 @@ +import apiClient from './index'; +import { toCamelCase } from './utils'; +import type { + SystemConfigConflictResponse, + SystemConfigResponse, + SystemConfigSchemaResponse, + SystemConfigValidationErrorResponse, + UpdateSystemConfigRequest, + UpdateSystemConfigResponse, + ValidateSystemConfigRequest, + ValidateSystemConfigResponse, +} from '../types/systemConfig'; + +type ApiErrorPayload = { + error?: string; + message?: string; + issues?: unknown; + current_config_version?: string; +}; + +export class SystemConfigValidationError extends Error { + issues: SystemConfigValidationErrorResponse['issues']; + + constructor(message: string, issues: SystemConfigValidationErrorResponse['issues']) { + super(message); + this.name = 'SystemConfigValidationError'; + this.issues = issues; + } +} + +export class SystemConfigConflictError extends Error { + currentConfigVersion?: string; + + constructor(message: string, currentConfigVersion?: string) { + super(message); + this.name = 'SystemConfigConflictError'; + this.currentConfigVersion = currentConfigVersion; + } +} + +function toSnakeUpdatePayload(payload: UpdateSystemConfigRequest): Record { + return { + config_version: payload.configVersion, + mask_token: payload.maskToken ?? '******', + reload_now: payload.reloadNow ?? true, + items: payload.items.map((item) => ({ + key: item.key, + value: item.value, + })), + }; +} + +function toSnakeValidatePayload(payload: ValidateSystemConfigRequest): Record { + return { + items: payload.items.map((item) => ({ + key: item.key, + value: item.value, + })), + }; +} + +function extractApiMessage(error: unknown, fallback: string): string { + if (!error || typeof error !== 'object' || !('response' in error)) { + return fallback; + } + + const response = (error as { response?: { data?: ApiErrorPayload } }).response; + return response?.data?.message || fallback; +} + +export const systemConfigApi = { + async getConfig(includeSchema = true): Promise { + const response = await apiClient.get>('/api/v1/system/config', { + params: { include_schema: includeSchema }, + }); + return toCamelCase(response.data); + }, + + async getSchema(): Promise { + const response = await apiClient.get>('/api/v1/system/config/schema'); + return toCamelCase(response.data); + }, + + async validate(payload: ValidateSystemConfigRequest): Promise { + const response = await apiClient.post>( + '/api/v1/system/config/validate', + toSnakeValidatePayload(payload), + ); + return toCamelCase(response.data); + }, + + async update(payload: UpdateSystemConfigRequest): Promise { + try { + const response = await apiClient.put>( + '/api/v1/system/config', + toSnakeUpdatePayload(payload), + ); + return toCamelCase(response.data); + } catch (error: unknown) { + if (error && typeof error === 'object' && 'response' in error) { + const status = (error as { response?: { status?: number } }).response?.status; + const payloadData = (error as { response?: { data?: ApiErrorPayload } }).response?.data; + + if (status === 400) { + const validationError = toCamelCase(payloadData ?? {}); + throw new SystemConfigValidationError( + validationError.message || '配置校验失败', + validationError.issues || [], + ); + } + + if (status === 409) { + const conflict = toCamelCase(payloadData ?? {}); + throw new SystemConfigConflictError( + conflict.message || '配置版本冲突', + conflict.currentConfigVersion, + ); + } + } + + throw new Error(extractApiMessage(error, '更新系统配置失败')); + } + }, +}; diff --git a/apps/dsa-web/src/api/utils.ts b/apps/dsa-web/src/api/utils.ts new file mode 100644 index 0000000..ac9ca6e --- /dev/null +++ b/apps/dsa-web/src/api/utils.ts @@ -0,0 +1,13 @@ +import camelcaseKeys from 'camelcase-keys'; + +/** + * 将 snake_case 对象键转换为 camelCase + * @param data API 响应数据 (snake_case) + * @returns 转换后的 camelCase 对象 + */ +export function toCamelCase(data: unknown): T { + if (data === null || data === undefined) { + return data as T; + } + return camelcaseKeys(data as Record, { deep: true }) as T; +} diff --git a/apps/dsa-web/src/assets/react.svg b/apps/dsa-web/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/apps/dsa-web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/dsa-web/src/components/common/Badge.tsx b/apps/dsa-web/src/components/common/Badge.tsx new file mode 100644 index 0000000..2f1b6c8 --- /dev/null +++ b/apps/dsa-web/src/components/common/Badge.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +type BadgeVariant = 'default' | 'success' | 'warning' | 'danger' | 'info' | 'history'; + +interface BadgeProps { + children: React.ReactNode; + variant?: BadgeVariant; + size?: 'sm' | 'md'; + glow?: boolean; + className?: string; +} + +const variantStyles: Record = { + default: 'bg-slate-700/50 text-gray-300 border-slate-600/50', + success: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30', + warning: 'bg-amber-500/20 text-amber-400 border-amber-500/30', + danger: 'bg-red-500/20 text-red-400 border-red-500/30', + info: 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30', + history: 'bg-purple-500/20 text-purple-400 border-purple-500/30', +}; + +const glowStyles: Record = { + default: '', + success: 'shadow-emerald-500/20', + warning: 'shadow-amber-500/20', + danger: 'shadow-red-500/20', + info: 'shadow-cyan-500/20', + history: 'shadow-purple-500/20', +}; + +/** + * 标签徽章组件 + * 支持多种变体和发光效果 + */ +export const Badge: React.FC = ({ + children, + variant = 'default', + size = 'sm', + glow = false, + className = '', +}) => { + const sizeStyles = size === 'sm' ? 'px-2 py-0.5 text-xs' : 'px-3 py-1 text-sm'; + + return ( + + {children} + + ); +}; diff --git a/apps/dsa-web/src/components/common/Button.tsx b/apps/dsa-web/src/components/common/Button.tsx new file mode 100644 index 0000000..cd118a4 --- /dev/null +++ b/apps/dsa-web/src/components/common/Button.tsx @@ -0,0 +1,121 @@ +import React from 'react'; + +interface ButtonProps extends React.ButtonHTMLAttributes { + variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'gradient' | 'danger'; + size?: 'sm' | 'md' | 'lg'; + isLoading?: boolean; + glow?: boolean; +} + +/** + * 按钮组件 + * 支持多种变体和科技感样式 + */ +export const Button: React.FC = ({ + children, + variant = 'primary', + size = 'md', + isLoading = false, + glow = false, + className = '', + disabled, + ...props +}) => { + const baseStyle = ` + inline-flex items-center justify-center + font-medium rounded-lg + transition-all duration-200 + focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-900 + disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none + `; + + const sizeStyles = { + sm: 'px-3 py-1.5 text-sm', + md: 'px-4 py-2.5 text-sm', + lg: 'px-6 py-3 text-base', + }; + + const variantStyles = { + primary: ` + bg-cyan-600 text-white + hover:bg-cyan-500 + focus:ring-cyan-500 + shadow-lg shadow-cyan-500/25 + `, + secondary: ` + bg-slate-700 text-gray-200 + hover:bg-slate-600 + focus:ring-slate-500 + border border-slate-600 + `, + outline: ` + bg-transparent text-cyan-400 + border border-cyan-500/30 + hover:bg-cyan-500/10 hover:border-cyan-500/50 + focus:ring-cyan-500 + `, + ghost: ` + bg-transparent text-gray-300 + hover:bg-white/5 hover:text-white + focus:ring-gray-500 + `, + gradient: ` + bg-gradient-to-r from-cyan-500 to-blue-500 text-white + hover:from-cyan-400 hover:to-blue-400 + focus:ring-cyan-500 + shadow-lg shadow-cyan-500/25 + `, + danger: ` + bg-red-600 text-white + hover:bg-red-500 + focus:ring-red-500 + shadow-lg shadow-red-500/25 + `, + }; + + const glowStyles = glow + ? 'shadow-glow-cyan hover:shadow-[0_0_30px_rgba(6,182,212,0.4)]' + : ''; + + return ( + + ); +}; diff --git a/apps/dsa-web/src/components/common/Card.tsx b/apps/dsa-web/src/components/common/Card.tsx new file mode 100644 index 0000000..709cbcf --- /dev/null +++ b/apps/dsa-web/src/components/common/Card.tsx @@ -0,0 +1,92 @@ +import type React from 'react'; + +interface CardProps { + title?: string; + subtitle?: string; + children: React.ReactNode; + className?: string; + variant?: 'default' | 'bordered' | 'gradient'; + hoverable?: boolean; + padding?: 'none' | 'sm' | 'md' | 'lg'; +} + +/** + * 终端风格卡片组件 + * 支持渐变边框、悬浮效果 + */ +export const Card: React.FC = ({ + title, + subtitle, + children, + className = '', + variant = 'default', + hoverable = false, + padding = 'md', +}) => { + const paddingStyles = { + none: '', + sm: 'p-3', + md: 'p-4', + lg: 'p-5', + }; + + const baseStyles = 'rounded-2xl'; + + const variantStyles = { + default: 'terminal-card', + bordered: 'terminal-card terminal-card-hover', + gradient: 'gradient-border-card', + }; + + const hoverStyles = hoverable + ? 'terminal-card-hover cursor-pointer' + : ''; + + if (variant === 'gradient') { + return ( +
+
+ {(title || subtitle) && ( +
+ {subtitle && ( + {subtitle} + )} + {title && ( +

+ {title} +

+ )} +
+ )} + {children} +
+
+ ); + } + + return ( +
+ {(title || subtitle) && ( +
+ {subtitle && ( + {subtitle} + )} + {title && ( +

+ {title} +

+ )} +
+ )} + {children} +
+ ); +}; diff --git a/apps/dsa-web/src/components/common/Collapsible.tsx b/apps/dsa-web/src/components/common/Collapsible.tsx new file mode 100644 index 0000000..11e074d --- /dev/null +++ b/apps/dsa-web/src/components/common/Collapsible.tsx @@ -0,0 +1,69 @@ +import React, { useState } from 'react'; + +interface CollapsibleProps { + title: string; + children: React.ReactNode; + defaultOpen?: boolean; + icon?: React.ReactNode; + className?: string; +} + +/** + * 可折叠面板组件 + * 支持动画展开/收起 + */ +export const Collapsible: React.FC = ({ + title, + children, + defaultOpen = false, + icon, + className = '', +}) => { + const [isOpen, setIsOpen] = useState(defaultOpen); + + return ( +
+ {/* 标题栏 */} + + + {/* 内容区 */} +
+
+ {children} +
+
+
+ ); +}; diff --git a/apps/dsa-web/src/components/common/Drawer.tsx b/apps/dsa-web/src/components/common/Drawer.tsx new file mode 100644 index 0000000..6179251 --- /dev/null +++ b/apps/dsa-web/src/components/common/Drawer.tsx @@ -0,0 +1,91 @@ +import type React from 'react'; +import { useEffect, useCallback } from 'react'; + +interface DrawerProps { + isOpen: boolean; + onClose: () => void; + title?: string; + children: React.ReactNode; + width?: string; +} + +/** + * 侧滑抽屉组件 - 终端风格 + */ +export const Drawer: React.FC = ({ + isOpen, + onClose, + title, + children, + width = 'max-w-2xl', +}) => { + // ESC 键关闭 + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }, + [onClose] + ); + + useEffect(() => { + if (isOpen) { + document.addEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'hidden'; + } + return () => { + document.removeEventListener('keydown', handleKeyDown); + document.body.style.overflow = ''; + }; + }, [isOpen, handleKeyDown]); + + if (!isOpen) return null; + + return ( +
+ {/* 遮罩层 */} +
+ + {/* 抽屉内容 */} +
+
+ {/* 头部 */} +
+ {title && ( +
+ DETAIL VIEW +

+ {title} +

+
+ )} + +
+ + {/* 内容区 */} +
+ {children} +
+
+
+
+ ); +}; diff --git a/apps/dsa-web/src/components/common/JsonViewer.tsx b/apps/dsa-web/src/components/common/JsonViewer.tsx new file mode 100644 index 0000000..b00a5cc --- /dev/null +++ b/apps/dsa-web/src/components/common/JsonViewer.tsx @@ -0,0 +1,92 @@ +import React, { useState } from 'react'; + +interface JsonViewerProps { + data: Record | unknown[] | null | undefined; + maxHeight?: string; + className?: string; +} + +/** + * JSON 结构化展示组件 + * 支持语法高亮和折叠 + */ +export const JsonViewer: React.FC = ({ + data, + maxHeight = '400px', + className = '', +}) => { + const [copied, setCopied] = useState(false); + + if (!data) { + return ( +
暂无数据
+ ); + } + + const jsonString = JSON.stringify(data, null, 2); + + const handleCopy = async () => { + await navigator.clipboard.writeText(jsonString); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + // 简单的语法高亮 + const highlightJson = (json: string): React.ReactNode => { + return json.split('\n').map((line, index) => { + // 高亮 key + let highlighted = line.replace( + /"([^"]+)":/g, + '"$1":' + ); + // 高亮字符串值 + highlighted = highlighted.replace( + /: "([^"]*)"/g, + ': "$1"' + ); + // 高亮数字 + highlighted = highlighted.replace( + /: (-?\d+\.?\d*)/g, + ': $1' + ); + // 高亮布尔值和 null + highlighted = highlighted.replace( + /: (true|false|null)/g, + ': $1' + ); + + return ( +
+ ); + }); + }; + + return ( +
+ {/* 复制按钮 */} + + + {/* JSON 内容 */} +
+
+          {highlightJson(jsonString)}
+        
+
+
+ ); +}; diff --git a/apps/dsa-web/src/components/common/Loading.tsx b/apps/dsa-web/src/components/common/Loading.tsx new file mode 100644 index 0000000..2e09e7a --- /dev/null +++ b/apps/dsa-web/src/components/common/Loading.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +export const Loading: React.FC = () => { + return ( +
+
+
+ ); +}; diff --git a/apps/dsa-web/src/components/common/Pagination.tsx b/apps/dsa-web/src/components/common/Pagination.tsx new file mode 100644 index 0000000..d6f42b5 --- /dev/null +++ b/apps/dsa-web/src/components/common/Pagination.tsx @@ -0,0 +1,112 @@ +import type React from 'react'; + +interface PaginationProps { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; + className?: string; +} + +/** + * 分页组件 - 终端风格 + */ +export const Pagination: React.FC = ({ + currentPage, + totalPages, + onPageChange, + className = '', +}) => { + if (totalPages <= 1) return null; + + // 生成页码数组 + const getPageNumbers = (): (number | string)[] => { + const pages: (number | string)[] = []; + const delta = 2; + + for (let i = 1; i <= totalPages; i++) { + if ( + i === 1 || + i === totalPages || + (i >= currentPage - delta && i <= currentPage + delta) + ) { + pages.push(i); + } else if (pages[pages.length - 1] !== '...') { + pages.push('...'); + } + } + + return pages; + }; + + const PageButton: React.FC<{ + page: number | string; + isActive?: boolean; + disabled?: boolean; + onClick?: () => void; + children?: React.ReactNode; + }> = ({ page, isActive, disabled, onClick, children }) => { + const isEllipsis = page === '...'; + + if (isEllipsis) { + return ( + ... + ); + } + + return ( + + ); + }; + + return ( +
+ {/* 上一页 */} + onPageChange(currentPage - 1)} + > + + + + + + {/* 页码 */} + {getPageNumbers().map((page, index) => ( + typeof page === 'number' && onPageChange(page)} + /> + ))} + + {/* 下一页 */} + onPageChange(currentPage + 1)} + > + + + + +
+ ); +}; diff --git a/apps/dsa-web/src/components/common/ScoreGauge.tsx b/apps/dsa-web/src/components/common/ScoreGauge.tsx new file mode 100644 index 0000000..f90e97e --- /dev/null +++ b/apps/dsa-web/src/components/common/ScoreGauge.tsx @@ -0,0 +1,186 @@ +import type React from 'react'; +import { useState, useEffect, useRef } from 'react'; +import { getSentimentLabel } from '../../types/analysis'; + +interface ScoreGaugeProps { + score: number; + size?: 'sm' | 'md' | 'lg'; + showLabel?: boolean; + className?: string; +} + +/** + * 情绪评分仪表盘 - 发光环形进度条 + * 参考金融终端风格设计,带过渡动画 + */ +export const ScoreGauge: React.FC = ({ + score, + size = 'md', + showLabel = true, + className = '', +}) => { + // 动画状态 + const [animatedScore, setAnimatedScore] = useState(0); + const [displayScore, setDisplayScore] = useState(0); + const animationRef = useRef(null); + const prevScoreRef = useRef(0); + + // 动画效果 + useEffect(() => { + const startScore = prevScoreRef.current; + const endScore = score; + const duration = 1000; // 动画时长 ms + const startTime = performance.now(); + + const animate = (currentTime: number) => { + const elapsed = currentTime - startTime; + const progress = Math.min(elapsed / duration, 1); + + // 使用 easeOutCubic 缓动函数 + const easeOut = 1 - Math.pow(1 - progress, 3); + + const currentScore = startScore + (endScore - startScore) * easeOut; + setAnimatedScore(currentScore); + setDisplayScore(Math.round(currentScore)); + + if (progress < 1) { + animationRef.current = requestAnimationFrame(animate); + } else { + prevScoreRef.current = endScore; + } + }; + + animationRef.current = requestAnimationFrame(animate); + + return () => { + if (animationRef.current) { + cancelAnimationFrame(animationRef.current); + } + }; + }, [score]); + + const label = getSentimentLabel(score); + + // 尺寸配置 + const sizeConfig = { + sm: { width: 100, stroke: 8, fontSize: 'text-2xl', labelSize: 'text-xs', gap: 6 }, + md: { width: 140, stroke: 10, fontSize: 'text-4xl', labelSize: 'text-sm', gap: 8 }, + lg: { width: 180, stroke: 12, fontSize: 'text-5xl', labelSize: 'text-base', gap: 10 }, + }; + + const { width, stroke, fontSize, labelSize, gap } = sizeConfig[size]; + const radius = (width - stroke) / 2; + const circumference = 2 * Math.PI * radius; + + // 从顶部开始,显示 270 度(3/4 圆弧) + const arcLength = circumference * 0.75; + const progress = (animatedScore / 100) * arcLength; + + // 颜色映射 - 使用动画分数计算颜色过渡 + const getStrokeColor = (s: number) => { + if (s >= 60) return '#00d4ff'; // 青色 - 贪婪 + if (s >= 40) return '#a855f7'; // 紫色 - 中性 + return '#ff4466'; // 红色 - 恐惧 + }; + + const strokeColor = getStrokeColor(animatedScore); + const glowColor = `${strokeColor}66`; + + return ( +
+ {/* 标题 */} + {showLabel && ( + + 恐惧贪婪指数 + + )} + +
+ + + {/* 渐变定义 */} + + + + + + {/* 发光滤镜 */} + + + + + + + + + + {/* 背景轨道 - 3/4 圆弧 */} + + + {/* 发光层 */} + + + {/* 进度圆弧 */} + + + + {/* 中心数值 */} +
+ + {displayScore} + + {showLabel && ( + + {label.toUpperCase()} + + )} +
+
+
+ ); +}; diff --git a/apps/dsa-web/src/components/common/Select.tsx b/apps/dsa-web/src/components/common/Select.tsx new file mode 100644 index 0000000..090a125 --- /dev/null +++ b/apps/dsa-web/src/components/common/Select.tsx @@ -0,0 +1,80 @@ +import React from 'react'; + +interface SelectOption { + value: string; + label: string; +} + +interface SelectProps { + value: string; + onChange: (value: string) => void; + options: SelectOption[]; + label?: string; + placeholder?: string; + disabled?: boolean; + className?: string; +} + +/** + * 下拉选择器组件 + * 科技感样式 + */ +export const Select: React.FC = ({ + value, + onChange, + options, + label, + placeholder = '请选择', + disabled = false, + className = '', +}) => { + return ( +
+ {label && ( + + )} +
+ + + {/* 下拉箭头 */} +
+ + + +
+
+
+ ); +}; diff --git a/apps/dsa-web/src/components/common/index.ts b/apps/dsa-web/src/components/common/index.ts new file mode 100644 index 0000000..8872efb --- /dev/null +++ b/apps/dsa-web/src/components/common/index.ts @@ -0,0 +1,10 @@ +export * from './Button'; +export * from './Card'; +export * from './Loading'; +export * from './Drawer'; +export * from './Collapsible'; +export * from './ScoreGauge'; +export * from './JsonViewer'; +export * from './Select'; +export * from './Badge'; +export * from './Pagination'; diff --git a/apps/dsa-web/src/components/history/HistoryList.tsx b/apps/dsa-web/src/components/history/HistoryList.tsx new file mode 100644 index 0000000..022114c --- /dev/null +++ b/apps/dsa-web/src/components/history/HistoryList.tsx @@ -0,0 +1,160 @@ +import type React from 'react'; +import { useRef, useCallback, useEffect } from 'react'; +import type { HistoryItem } from '../../types/analysis'; +import { getSentimentColor } from '../../types/analysis'; +import { formatDateTime } from '../../utils/format'; + +interface HistoryListProps { + items: HistoryItem[]; + isLoading: boolean; + isLoadingMore: boolean; + hasMore: boolean; + selectedQueryId?: string; + onItemClick: (queryId: string) => void; + onLoadMore: () => void; + className?: string; +} + +/** + * 历史记录列表组件 + * 显示最近的股票分析历史,支持点击查看详情和滚动加载更多 + */ +export const HistoryList: React.FC = ({ + items, + isLoading, + isLoadingMore, + hasMore, + selectedQueryId, + onItemClick, + onLoadMore, + className = '', +}) => { + const scrollContainerRef = useRef(null); + const loadMoreTriggerRef = useRef(null); + + // 使用 IntersectionObserver 检测滚动到底部 + const handleObserver = useCallback( + (entries: IntersectionObserverEntry[]) => { + const target = entries[0]; + // 只有当触发器真正可见且有更多数据时才加载 + if (target.isIntersecting && hasMore && !isLoading && !isLoadingMore) { + // 确保容器有滚动能力(内容超过容器高度) + const container = scrollContainerRef.current; + if (container && container.scrollHeight > container.clientHeight) { + onLoadMore(); + } + } + }, + [hasMore, isLoading, isLoadingMore, onLoadMore] + ); + + useEffect(() => { + const trigger = loadMoreTriggerRef.current; + const container = scrollContainerRef.current; + if (!trigger || !container) return; + + const observer = new IntersectionObserver(handleObserver, { + root: container, + rootMargin: '20px', // 减小预加载距离 + threshold: 0.1, // 触发器至少 10% 可见时才触发 + }); + + observer.observe(trigger); + + return () => { + observer.disconnect(); + }; + }, [handleObserver]); + + return ( + + ); +}; diff --git a/apps/dsa-web/src/components/history/index.ts b/apps/dsa-web/src/components/history/index.ts new file mode 100644 index 0000000..37033f7 --- /dev/null +++ b/apps/dsa-web/src/components/history/index.ts @@ -0,0 +1 @@ +export { HistoryList } from './HistoryList'; diff --git a/apps/dsa-web/src/components/report/ReportDetails.tsx b/apps/dsa-web/src/components/report/ReportDetails.tsx new file mode 100644 index 0000000..95c7865 --- /dev/null +++ b/apps/dsa-web/src/components/report/ReportDetails.tsx @@ -0,0 +1,127 @@ +import type React from 'react'; +import { useState } from 'react'; +import type { ReportDetails as ReportDetailsType } from '../../types/analysis'; +import { Card } from '../common'; + +interface ReportDetailsProps { + details?: ReportDetailsType; + queryId?: string; +} + +/** + * 透明度与追溯区组件 - 终端风格 + */ +export const ReportDetails: React.FC = ({ + details, + queryId, +}) => { + const [showRaw, setShowRaw] = useState(false); + const [showSnapshot, setShowSnapshot] = useState(false); + const [copied, setCopied] = useState(false); + + if (!details?.rawResult && !details?.contextSnapshot && !queryId) { + return null; + } + + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Copy failed:', err); + } + }; + + const renderJson = (data: unknown) => { + const jsonStr = JSON.stringify(data, null, 2); + return ( +
+ +
+          {jsonStr}
+        
+
+ ); + }; + + return ( + +
+ TRANSPARENCY +

数据追溯

+
+ + {/* Query ID */} + {queryId && ( +
+ Query ID: + + {queryId} + +
+ )} + + {/* 折叠区域 */} +
+ {/* 原始分析结果 */} + {details?.rawResult && ( +
+ + {showRaw && ( +
+ {renderJson(details.rawResult)} +
+ )} +
+ )} + + {/* 分析快照 */} + {details?.contextSnapshot && ( +
+ + {showSnapshot && ( +
+ {renderJson(details.contextSnapshot)} +
+ )} +
+ )} +
+
+ ); +}; diff --git a/apps/dsa-web/src/components/report/ReportNews.tsx b/apps/dsa-web/src/components/report/ReportNews.tsx new file mode 100644 index 0000000..dbc04ab --- /dev/null +++ b/apps/dsa-web/src/components/report/ReportNews.tsx @@ -0,0 +1,137 @@ +import type React from 'react'; +import { useState, useEffect, useCallback } from 'react'; +import { Card } from '../common'; +import { historyApi } from '../../api/history'; +import type { NewsIntelItem } from '../../types/analysis'; + +interface ReportNewsProps { + queryId?: string; + limit?: number; +} + +/** + * 资讯区组件 - 终端风格 + */ +export const ReportNews: React.FC = ({ queryId, limit = 20 }) => { + const [isLoading, setIsLoading] = useState(false); + const [items, setItems] = useState([]); + const [error, setError] = useState(null); + + const fetchNews = useCallback(async () => { + if (!queryId) return; + setIsLoading(true); + setError(null); + + try { + const response = await historyApi.getNews(queryId, limit); + setItems(response.items || []); + } catch (err) { + setError(err instanceof Error ? err.message : '加载资讯失败'); + } finally { + setIsLoading(false); + } + }, [queryId, limit]); + + useEffect(() => { + setItems([]); + setError(null); + + if (queryId) { + fetchNews(); + } + }, [queryId, fetchNews]); + + if (!queryId) { + return null; + } + + return ( + +
+
+ NEWS FEED +

相关资讯

+
+
+ {isLoading && ( +
+ )} + +
+
+ + {error && !isLoading && ( +
+ {error} + +
+ )} + + {isLoading && !error && ( +
+
+ 加载资讯中... +
+ )} + + {!isLoading && !error && items.length === 0 && ( +
暂无相关资讯
+ )} + + {!isLoading && !error && items.length > 0 && ( +
+ {items.map((item, index) => ( +
+
+
+

+ {item.title} +

+ {item.snippet && ( +

+ {item.snippet} +

+ )} +
+ {item.url && ( + + 跳转 + + + + + )} +
+
+ ))} + +
+ )} + + ); +}; diff --git a/apps/dsa-web/src/components/report/ReportOverview.tsx b/apps/dsa-web/src/components/report/ReportOverview.tsx new file mode 100644 index 0000000..86f2fda --- /dev/null +++ b/apps/dsa-web/src/components/report/ReportOverview.tsx @@ -0,0 +1,133 @@ +import type React from 'react'; +import type { ReportMeta, ReportSummary as ReportSummaryType } from '../../types/analysis'; +import { ScoreGauge, Card } from '../common'; +import { formatDateTime } from '../../utils/format'; + +interface ReportOverviewProps { + meta: ReportMeta; + summary: ReportSummaryType; + isHistory?: boolean; +} + +/** + * 报告概览区组件 - 终端风格 + */ +export const ReportOverview: React.FC = ({ + meta, + summary +}) => { + // 根据涨跌幅获取颜色 + const getPriceChangeColor = (changePct: number | undefined): string => { + if (changePct === undefined || changePct === null) return 'text-muted'; + if (changePct > 0) return 'text-[#ff4d4d]'; // 红涨 + if (changePct < 0) return 'text-[#00d46a]'; // 绿跌 + return 'text-muted'; + }; + + // 格式化涨跌幅 + const formatChangePct = (changePct: number | undefined): string => { + if (changePct === undefined || changePct === null) return '--'; + const sign = changePct > 0 ? '+' : ''; + return `${sign}${changePct.toFixed(2)}%`; + }; + + return ( +
+ {/* 主信息区 - 两列布局 */} +
+ {/* 左侧:股票信息与结论 */} +
+ {/* 股票头部 */} + +
+
+
+

+ {meta.stockName || meta.stockCode} +

+ {/* 价格和涨跌幅 */} + {meta.currentPrice != null && ( +
+ + {meta.currentPrice.toFixed(2)} + + + {formatChangePct(meta.changePct)} + +
+ )} +
+
+ + {meta.stockCode} + + + + + + {formatDateTime(meta.createdAt)} + +
+
+
+ + {/* 关键结论 */} +
+ KEY INSIGHTS +

+ {summary.analysisSummary || '暂无分析结论'} +

+
+
+ + {/* 操作建议和趋势预测 */} +
+ {/* 操作建议 */} + +
+
+ + + +
+
+

操作建议

+

+ {summary.operationAdvice || '暂无建议'} +

+
+
+
+ + {/* 趋势预测 */} + +
+
+ + + +
+
+

趋势预测

+

+ {summary.trendPrediction || '暂无预测'} +

+
+
+
+
+
+ + {/* 右侧:情绪指标 */} +
+ +
+

Market Sentiment

+ +
+
+
+
+
+ ); +}; diff --git a/apps/dsa-web/src/components/report/ReportStrategy.tsx b/apps/dsa-web/src/components/report/ReportStrategy.tsx new file mode 100644 index 0000000..1561b3c --- /dev/null +++ b/apps/dsa-web/src/components/report/ReportStrategy.tsx @@ -0,0 +1,82 @@ +import type React from 'react'; +import type { ReportStrategy as ReportStrategyType } from '../../types/analysis'; +import { Card } from '../common'; + +interface ReportStrategyProps { + strategy?: ReportStrategyType; +} + +interface StrategyItemProps { + label: string; + value?: string; + color: string; +} + +const StrategyItem: React.FC = ({ + label, + value, + color, +}) => ( +
+
+ {label} + + {value || '—'} + +
+ {/* 底部指示条 */} +
+
+); + +/** + * 策略点位区组件 - 终端风格 + */ +export const ReportStrategy: React.FC = ({ strategy }) => { + if (!strategy) { + return null; + } + + const strategyItems = [ + { + label: '理想买入', + value: strategy.idealBuy, + color: '#00ff88', // success + }, + { + label: '二次买入', + value: strategy.secondaryBuy, + color: '#00d4ff', // cyan + }, + { + label: '止损价位', + value: strategy.stopLoss, + color: '#ff4466', // danger + }, + { + label: '止盈目标', + value: strategy.takeProfit, + color: '#ffaa00', // warning + }, + ]; + + return ( + +
+ STRATEGY POINTS +

狙击点位

+
+
+ {strategyItems.map((item) => ( + + ))} +
+
+ ); +}; diff --git a/apps/dsa-web/src/components/report/ReportSummary.tsx b/apps/dsa-web/src/components/report/ReportSummary.tsx new file mode 100644 index 0000000..6b981b2 --- /dev/null +++ b/apps/dsa-web/src/components/report/ReportSummary.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import type { AnalysisResult, AnalysisReport } from '../../types/analysis'; +import { ReportOverview } from './ReportOverview'; +import { ReportStrategy } from './ReportStrategy'; +import { ReportNews } from './ReportNews'; +import { ReportDetails } from './ReportDetails'; + +interface ReportSummaryProps { + data: AnalysisResult | AnalysisReport; + isHistory?: boolean; +} + +/** + * 完整报告展示组件 + * 整合概览、策略、资讯、详情四个区域 + */ +export const ReportSummary: React.FC = ({ + data, + isHistory = false, +}) => { + // 兼容 AnalysisResult 和 AnalysisReport 两种数据格式 + const report: AnalysisReport = 'report' in data ? data.report : data; + const queryId = 'queryId' in data ? data.queryId : report.meta.queryId; + + const { meta, summary, strategy, details } = report; + + return ( +
+ {/* 概览区(首屏) */} + + + {/* 策略点位区 */} + + + {/* 资讯区 */} + + + {/* 透明度与追溯区 */} + +
+ ); +}; diff --git a/apps/dsa-web/src/components/report/index.ts b/apps/dsa-web/src/components/report/index.ts new file mode 100644 index 0000000..8b37228 --- /dev/null +++ b/apps/dsa-web/src/components/report/index.ts @@ -0,0 +1,5 @@ +export * from './ReportSummary'; +export * from './ReportOverview'; +export * from './ReportStrategy'; +export * from './ReportNews'; +export * from './ReportDetails'; diff --git a/apps/dsa-web/src/components/settings/SettingsAlert.tsx b/apps/dsa-web/src/components/settings/SettingsAlert.tsx new file mode 100644 index 0000000..3d694eb --- /dev/null +++ b/apps/dsa-web/src/components/settings/SettingsAlert.tsx @@ -0,0 +1,37 @@ +import type React from 'react'; + +interface SettingsAlertProps { + title: string; + message: string; + variant?: 'error' | 'success' | 'warning'; + actionLabel?: string; + onAction?: () => void; + className?: string; +} + +const variantStyles: Record, string> = { + error: 'border-red-500/35 bg-red-500/10 text-red-200', + success: 'border-emerald-500/35 bg-emerald-500/10 text-emerald-200', + warning: 'border-amber-500/35 bg-amber-500/10 text-amber-200', +}; + +export const SettingsAlert: React.FC = ({ + title, + message, + variant = 'error', + actionLabel, + onAction, + className = '', +}) => { + return ( +
+

{title}

+

{message}

+ {actionLabel && onAction ? ( + + ) : null} +
+ ); +}; diff --git a/apps/dsa-web/src/components/settings/SettingsField.tsx b/apps/dsa-web/src/components/settings/SettingsField.tsx new file mode 100644 index 0000000..167c724 --- /dev/null +++ b/apps/dsa-web/src/components/settings/SettingsField.tsx @@ -0,0 +1,236 @@ +import { useState } from 'react'; +import type React from 'react'; +import { Select } from '../common'; +import type { ConfigValidationIssue, SystemConfigItem } from '../../types/systemConfig'; +import { getFieldDescriptionZh, getFieldTitleZh } from '../../utils/systemConfigI18n'; + +function isMultiValueField(item: SystemConfigItem): boolean { + const validation = (item.schema?.validation ?? {}) as Record; + return Boolean(validation.multiValue ?? validation.multi_value); +} + +function parseMultiValues(value: string): string[] { + if (!value) { + return ['']; + } + + const values = value.split(',').map((entry) => entry.trim()); + return values.length ? values : ['']; +} + +function serializeMultiValues(values: string[]): string { + return values.map((entry) => entry.trim()).join(','); +} + +interface SettingsFieldProps { + item: SystemConfigItem; + value: string; + disabled?: boolean; + onChange: (key: string, value: string) => void; + issues?: ConfigValidationIssue[]; +} + +function renderFieldControl( + item: SystemConfigItem, + value: string, + disabled: boolean, + onChange: (nextValue: string) => void, + isSecretVisible: boolean, + onToggleSecretVisible: () => void, +) { + const schema = item.schema; + const commonClass = 'input-terminal'; + const controlType = schema?.uiControl ?? 'text'; + const isMultiValue = isMultiValueField(item); + + if (controlType === 'textarea') { + return ( +