├── LICENSE ├── README.md ├── config ├── dependencies_config.json ├── dependency_cache.json └── python_environments.json ├── css ├── main.css └── modules │ ├── base.css │ ├── components.css │ └── utilities.css ├── index.html ├── js ├── main.js └── modules │ ├── core-api.js │ ├── dependency-manager.js │ ├── ui-components.js │ └── visualization.js ├── main.py ├── py ├── __init__.py ├── api.py ├── core.py └── dependency.py ├── requirements.txt └── 启动.bat /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Dontdrunk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI环境管理工具 2 | 3 | https://github.com/user-attachments/assets/87a2be3f-c9d3-46be-9526-ace0223d7330 4 | 5 | 一个简洁高效的 Python AI 开发环境管理工具,提供可视化界面进行 Python 依赖包的管理和维护。 6 | 7 | ## 特色功能 8 | 9 | 1. **全面的依赖概览** 10 | - 清晰展示所有已安装的 Python 依赖 11 | - 实时检查并显示最新版本状态 12 | - 智能分类标记系统、AI模型和数据科学相关依赖 13 | - 自动获取并显示依赖的详细描述信息 14 | 15 | 2. **高效的依赖管理** 16 | - 一键安装、卸载和更新依赖 17 | - 支持版本历史浏览和快速切换 18 | - 批量操作多个依赖 19 | - 支持上传安装 wheel 文件和 requirements.txt 20 | 21 | 3. **优化的用户体验** 22 | - 实时搜索和多条件筛选 23 | - 明暗主题切换 24 | - 响应式界面设计 25 | - 操作进度可视化 26 | 27 | ## 使用方法 28 | 29 | 1. 双击 `启动.bat` 文件运行应用 30 | 2. 在浏览器中访问 `http://127.0.0.1:8282` 打开管理界面 31 | 3. 应用将自动加载所有已安装的 Python 依赖 32 | 33 | ### 依赖搜索和筛选 34 | 35 | - 使用顶部搜索框快速查找依赖 36 | - 使用下拉菜单按类别筛选(全部、核心、软件、数据科学、人工智能、其他) 37 | 38 | ### 依赖安装 39 | 40 | - 在底部安装区域输入包名称直接安装 41 | - 支持指定版本号(例如:`tensorflow==2.15.0`) 42 | - 上传 .whl 文件进行本地安装 43 | - 上传 requirements.txt 批量安装 44 | 45 | ### 批量操作 46 | 47 | - 使用复选框选择多个依赖 48 | - 点击"批量卸载"一次性移除多个依赖 49 | - 点击"一键更新所选依赖"将所选依赖更新到最新版本 50 | - 点击"清理PIP缓存"快速释放存储空间 51 | 52 | ## 多环境管理 53 | - 可设置指定目录环境进行管理,支持虚拟环境和便携式环境 54 | 55 | ## 系统要求 56 | 57 | - Windows 操作系统 58 | - Python 3.7+ 59 | - 现代浏览器(推荐 Chrome、Edge 或 Firefox 最新版本) 60 | 61 | ## 故障排除 62 | 63 | 如果遇到问题: 64 | 65 | 1. 确保您的Python环境正常工作 66 | 2. 检查是否有防火墙阻止了应用程序 67 | 3. 确保端口8282未被其他应用占用 68 | 4. 如有问题,请查看控制台输出的错误信息 69 | 5. 如果一直停留在加载界面,请检查自己的网络,确保魔法畅通。 70 | 71 | ## 更新日志 72 | 73 | ### V2.1.0-正式版 (2025-05-27) 74 | 75 | **问题修复** 🐛 76 | - 修复前后端通信BUG,解决依赖更新后前端界面无法刷新的问题 77 | - 优化网络并发请求机制,大幅提高批量操作时的稳定性和响应速度 78 | - 完善错误处理机制,提升系统整体稳定性 79 | 80 | ### V2.0.0-Bata (2025-05-24) 81 | 82 | **新特性** ✨ 83 | - 全新的用户界面设计,提供更直观的操作体验 84 | - 多环境管理,支持便携式环境丶虚拟环境丶系统环境管理(支持添加多个环境) 85 | 86 | **优化改进** 🔨 87 | - 优化依赖安装流程,提升安装成功率 88 | - 优化批量操作功能,提供更清晰的进度展示 89 | - 提升整体性能和响应速度 90 | 91 | **问题修复** 🐛 92 | - 修复某些依赖无法正确显示版本信息的问题 93 | - 修复在特定情况下安装wheel文件失败的问题 94 | - 解决requirements.txt导入时的编码问题 95 | - 修复批量更新时可能出现的并发问题 96 | - 修复无法正确获取描述的问题 97 | 98 | **待修复问题** (该问题将在下个版本修复) 99 | - 前后端通信存在一定BUG,更新依赖后前端界面有时会出现无法刷新的情况,需要刷新浏览器才能查看最新结果。 100 | - 网络并发请求还有近一步优化的空间 101 | 102 | ### V1.0.0 (2025-04-02) 103 | 104 | - 最初版本上线,支持管理本地Python环境 105 | 106 | ## 支持作者 107 | 108 | 🎭 我用代码垒起一座围栏,把Bug都关在了里面... 109 | 但是!Bug总会顽皮地翻墙逃出来,我需要你的支持来买更多的砖头补墙! 110 | 111 | 以下是我的"防Bug补墙基金"募捐通道 👇 112 | 113 | 💡 捐赠小贴士: 114 | - ¥6.66 - 助我喝杯咖啡,让Bug没机会钻空子 115 | - ¥66.6 - 够我吃顿好的,代码写得更欢乐 116 | - ¥666 - 我要给Bug们建豪华监狱! 117 | 118 | 🎯 声明:本项目开源免费,打赏纯自愿。 119 | 不打赏也没关系,但你的程序可能会遇到一些"不经意的小惊喜"... 120 | 121 | ![收款码](https://github.com/user-attachments/assets/f7fde32c-83b9-4c4b-8e4b-e6192ee34cec) 122 | 123 | (开玩笑的,绝对不会有Bug😉) 124 | 125 | -------------------------------------------------------------------------------- /config/dependencies_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "systemDependencies": [ 3 | "pip", "setuptools", "wheel", "distlib", "virtualenv", "blinker", 4 | "six", "soupsieve", "webencodings", "colorama", "certifi", 5 | "cffi", "chardet", "idna", "urllib3", "charset-normalizer", 6 | "pycparser", "cryptography", "pyopenssl", "pygments", "pyparsing", 7 | "packaging", "MarkupSafe", "Jinja2", "pytz", "toml", "platformdirs", 8 | "importlib-metadata", "zipp", "click", "werkzeug", "flask", "flask-cors", 9 | "itsdangerous", "filelock", "typing-extensions", "decorator", "entrypoints", 10 | "pywin32", "pythonnet", "psutil", "pyyaml", "atomicwrites", "attrs", "distro", 11 | "pluggy", "jsonschema", "pyrsistent", "more-itertools", "python-dateutil", "requests", 12 | "torch", "torchvision", "torchaudio" 13 | ], 14 | "softwareDependencies": [ 15 | "flask", "flask-cors", "requests", "packaging", "importlib-metadata", "werkzeug", 16 | "jinja2", "itsdangerous", "click", "markupsafe", "colorama", "pipdeptree" 17 | ], 18 | "dataScienceDependencies": [ 19 | "numpy", "scipy", "pandas", "scikit-learn", "statsmodels", "joblib", 20 | "numba", "dask", "vaex", "pillow", "matplotlib", "seaborn", "plotly", 21 | "bokeh", "altair", "graphviz", "networkx", 22 | "sqlalchemy", "psycopg2", "pymysql", "pymongo", "redis", "elasticsearch", 23 | "cassandra-driver", "neo4j", "clickhouse-driver", "mariadb", 24 | "polars", "modin", "datatable", "koalas", "cudf", "cupy", "datashader", 25 | "holoviews", "pygwalker", "sweetviz", "pandas-profiling", "dtale", 26 | "lux-api", "hiplot", "mplfinance", "cufflinks", "yellowbrick", "eli5", 27 | "dalex", "pycebox", "scikit-optimize", "hyperopt", "nevergrad", 28 | "ax-platform", "bayesian-optimization", "scikit-multilearn", "river", 29 | "creme", "imbalanced-learn", "tsfel", "tsfresh", "cesium", "featuretools", 30 | "feature-engine", "category_encoders", "patsy", "kmodes", "sompy", 31 | "hdbscan", "umap-learn", "pacmap", "alluvial-diagrams", "spectral-clustering", 32 | "dbscan", "opentsne", "interpret", "interpret-ml", "pdpbox", "fairlearn", 33 | "fairness-indicators", "optuna-dashboard", "mlemory-profiler", "bandit", 34 | "skll", "dvc", "great_expectations", "cerberus", "pandera", "arctic", 35 | "ibis-framework", "pyarrow", "duckdb", "modin", "daft", "lancedb", 36 | "milvus", "vespa", "qdrant", "scylladb", "tidb", "druid", 37 | "clickhouse-sqlalchemy", "influxdb", "timescaledb", "monetdb", "teradata", 38 | "phoenixdb", "kinetica", "skdb", "qlib", "pyfolio", "finmarketpy", 39 | "backtrader", "zipline", "alphalens", "ta-lib", "ta", "mlfinlab", 40 | "empyrical", "pyalgotrade", "backtesting", "quantstats", 41 | "adtk", "altair_ally", "anndata", "arviz", "autoimpute", "autoviz", 42 | "bamboolib", "bioinfokit", "biopython", "blaze", "bonobo", "bqplot", 43 | "dagster", "darts-forecast", "datacompy", "dataprep", "datasette", 44 | "deepchecks", "deepnog", "deltalake", "evidently", "factor_analyzer", 45 | "fastparquet", "feast", "geoalchemy2", "geopy", "glom", "gplearn", 46 | "gspread", "ipywidgets", "janitor", "kedro", "keplergl", "klib", 47 | "lifelines", "luigi", "lux", "mlxtend", "momepy", "nbconvert", 48 | "nilearn", "nlpaug", "omegaconf", "ortools", "osmnx", "prefect", 49 | "prov", "pyjanitor", "pymc-marketing", "pymde", "pymongo-pandas", 50 | "pytensor", "pyviz", "pyxll", "qgrid", "shiny", "skimpy", 51 | "statsforecast", "sympy", "tableone", "tabulate", "trubrics", 52 | "tune-sklearn", "ucimlrepo", "upath", "vaex-viz", "visidata", 53 | "voila-gridstack", "vtk", "xarray-spatial", "xarray", "xgboost-ray", 54 | "xlrd", "xlwings", "ydata-profiling", "zarr" 55 | ], 56 | "aiDependencies": [ 57 | "accelerate", "bitsandbytes", "triton", "peft", "deepspeed", 58 | "optimum", "llama-cpp-python", "AutoGPTQ", "flash-attn", 59 | "vllm", "ctransformers", "exllama", "gradio", "streamlit", 60 | "fastapi", "sentence-transformers", "langchain", "llama-index", 61 | "transformers", "diffusers", "pytorch-lightning", "einops", 62 | "safetensors", "tokenizers", "datasets", "evaluate", "loralib", 63 | "optimum", "timm", "wandb", "mlflow", "optuna", "hydra-core", 64 | "onnx", "onnxruntime", "onnxruntime-gpu", "tensorboardX", 65 | "huggingface_hub", "model-index", "hf-transfer", "accelerator", 66 | "torch", "torchvision", "torchaudio", 67 | "tensorflow", "tensorflow-gpu", 68 | "keras", "theano", "jax", "flax", "mxnet", "caffe", "paddlepaddle", 69 | "nltk", "spacy", "gensim", "pattern", "textblob", "polyglot", 70 | "stanza", "huggingface_hub", "tokenizers", 71 | "transformers-interpret", "jieba", "fasttext", "allennlp", "fairseq", 72 | "flair", "bert-serving-server", "bert-serving-client", "rasa", 73 | "adapter-transformers", "sentence-splitter", "text2vec", "keybert", 74 | "opencv-python", "scikit-image", "mahotas", "imageio", "albumentations", 75 | "kornia", "pytesseract", "pywavelets", 76 | "opencv-python-headless", "detectron2", "mmcv", "mmdet", "mmseg", 77 | "supervision", "roboflow", "yolov5", "ultralytics", "facenet-pytorch", 78 | "dlib", "mediapipe", "face-recognition", "imgaug", "kornia", 79 | "xgboost", "lightgbm", "catboost", "pymc", "pystan", "prophet", 80 | "ray", "tune", "horovod", "fairscale", "thinc", 81 | "dash", "streamlit", "panel", "voila", "gradio", "pydantic", "starlette", 82 | "stable-diffusion-webui", "audioldm", "audiocraft", "bark", 83 | "comfyui", "taming-transformers", "coqui-tts", "open-clip", 84 | "whisper", "speechbrain", "moviepy", 85 | "gymnasium", "gym", "stable-baselines3", "tianshou", "rlgym", "spinningup", 86 | "langflow", "llm", "modelscope", "text-generation-webui", "fastchat", 87 | "mosaicml", "trl", "megatron-lm", "nanotron", "llamaindex", "bentoml", 88 | "mlflow", "ray[tune]", 89 | "auto-gptq", "exllama", "gptq-for-llama", "llama.cpp", "flashattention", 90 | "onnx-runtime", "tvm", "tensorrt", 91 | "autokeras", "fastai", "segmentation-models", "yolact", "efficientnet", 92 | "resnest", "deepspeech", "librosa", "spleeter", "demucs", "pyannote-audio", 93 | "autoencoders", "openai-whisper", "rl-games", "mindsdb", "pyro-ppl", 94 | "edward", "bambi", "numpyro", "pymc3", "bert-extractive-summarizer", 95 | "summarizer", "sumy", "txtai", "lexnlp", "snorkel", "flyingsquid", 96 | "cleanlab", "focal-loss", "pix2pix", "stylegan", "dcgan", "cycleGAN", 97 | "nerfstudio", "instant-ngp", "habitat-sim", "mujoco-py", "dm_control", 98 | "opensim", "neuralmonkey", "lime", "shap", "alibi", "aif360", "deepchecks", 99 | "evidently", "autogluon", "pycaret", "sktime", "neuralprophet", "gluonts", 100 | "kats", "tslearn", "pyts", "darts", "pmdarima", "orbit-ml", "etna", "tempo", 101 | "torchani", "deepchem", "rdkit", "pymatgen", "psi4", "ase", "matscipy", 102 | "openmm", "botfront", "botbuilder-core", "chatterbot", "nemoguardrails", 103 | "autogluon-tabular", "biobert", "cleanvision", "cleverhans", "cylp", 104 | "deeplake", "deeplip", "deeppavlov", "dgl", "distilbert", "ecco", 105 | "efficientnet-pytorch", "eleuther-ai", "espnet", "fewshot-learning", 106 | "flaml", "frankenstein", "gluon", "gorilla", "gradsim", "grimoireml", 107 | "h2o", "haystack", "hummingbird", "hyperband", "hyperparameter-hunter", 108 | "ignite", "imgviz", "interpret-community", "judge", "kubeflow", 109 | "labml", "laion", "lark-parser", "ludwig", "metaflow", "miniai", 110 | "mmocr", "modAL", "nebuly", "nemo", "nnabla", "onnxmltools", 111 | "opacus", "openvino", "petastorm", "piccolotto", "pytorch3d", 112 | "pywick", "qiskit", "quantus", "recommenders", "replay", "rllib", 113 | "robustdg", "sagemaker", "scikeras", "seldon-core", "shapash", 114 | "skorch", "snips-nlu", "spago", "splade", "tensorflow-addons", 115 | "tensorflow-decision-forests", "tensorflow-probability", "tensorflow-serving", 116 | "torchserve", "trax", "vectorhub", "vowpal-wabbit", "xformers", "zenml","tqwen-vl-utils", 117 | "huggingface-hub","gradio_client" 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /config/dependency_cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "typing_extensions": "Backported and Experimental Type Hints for Python 3.8+", 3 | "accelerate": "Accelerate", 4 | "typing-inspection": "Runtime typing introspection tools", 5 | "tzdata": "Provider of IANA time zone data", 6 | "aiofiles": "File support for asyncio.", 7 | "urllib3": "HTTP library with thread-safe connection pooling, file post, and more.", 8 | "uvicorn": "The lightning-fast ASGI server.", 9 | "annotated-types": "Reusable constraint types to use with typing.Annotated", 10 | "anyio": "High level compatibility layer for multiple asynchronous event loop implementations", 11 | "attrs": "Classes Without Boilerplate", 12 | "beautifulsoup4": "Screen-scraping library", 13 | "webcolors": "A library for working with the color formats defined by HTML and CSS.", 14 | "webencodings": "Character encoding aliases for legacy web content", 15 | "websockets": "An implementation of the WebSocket Protocol (RFC 6455 & 7692)", 16 | "wget": "pure python download utility", 17 | "blinker": "Fast, simple object-to-object and broadcast signaling", 18 | "certifi": "Python package for providing Mozilla's CA Bundle.", 19 | "zipp": "Backport of pathlib-compatible object wrapper for zip files", 20 | "cffi": "Foreign Function Interface for Python calling C code.", 21 | "chardet": "Universal encoding detector for Python 3", 22 | "charset-normalizer": "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet.", 23 | "click": "Composable command line interface toolkit", 24 | "cmake": "CMake is an open-source, cross-platform family of tools designed to build, test and package software", 25 | "colorama": "Cross-platform colored terminal text.", 26 | "coloredlogs": "Colored terminal output for Python's logging module", 27 | "cryptography": "cryptography is a package which provides cryptographic recipes and primitives to Python developers.", 28 | "cycler": "Composable style cycles", 29 | "datasets": "HuggingFace community-driven open-source library of datasets", 30 | "diffusers": "State-of-the-art diffusion in PyTorch and JAX.", 31 | "distro": "Distro - an OS platform information API", 32 | "einops": "A new flavour of deep learning operations", 33 | "fastapi": "FastAPI framework, high performance, easy to learn, fast to code, ready for production", 34 | "ffmpy": "A simple Python wrapper for FFmpeg", 35 | "filelock": "A platform independent file lock.", 36 | "flatbuffers": "The FlatBuffers serialization format for Python", 37 | "fonttools": "Tools to manipulate font files", 38 | "frozenlist": "A list-like structure which implements collections.abc.MutableSequence", 39 | "fsspec": "File-system specification", 40 | "gradio": "Python library for easily interacting with trained machine learning models", 41 | "gradio_client": "Python library for easily interacting with trained machine learning models", 42 | "groovy": "A small Python library created to help developers protect their applications from Server Side Request Forgery (SSRF) attacks.", 43 | "h11": "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1", 44 | "httpcore": "A minimal low-level HTTP client.", 45 | "httpx": "The next generation HTTP client.", 46 | "httpx-sse": "Consume Server-Sent Event (SSE) messages with HTTPX.", 47 | "huggingface-hub": "Client library to download and publish models, datasets and other repos on the huggingface.co hub", 48 | "humanfriendly": "Human friendly output for text interfaces using Python", 49 | "idna": "Internationalized Domain Names in Applications (IDNA)", 50 | "importlib_metadata": "Read metadata from Python packages", 51 | "jinja2": "A very fast and expressive template engine.", 52 | "jiter": "Fast iterable JSON parser.", 53 | "joblib": "Lightweight pipelining with Python functions", 54 | "kiwisolver": "A fast implementation of the Cassowary constraint solver", 55 | "lxml": "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API.", 56 | "markdown-it-py": "Python port of markdown-it. Markdown parsing, done right!", 57 | "markupsafe": "Safely add untrusted strings to HTML/XML markup.", 58 | "matplotlib": "Python plotting package", 59 | "mdurl": "Markdown URL utilities", 60 | "mpmath": "Python library for arbitrary-precision floating-point arithmetic", 61 | "multidict": "multidict implementation", 62 | "networkx": "Python package for creating and manipulating graphs and networks", 63 | "numpy": "Fundamental package for array computing in Python", 64 | "ollama": "The official Python client for Ollama.", 65 | "onnxruntime": "ONNX Runtime is a runtime accelerator for Machine Learning models", 66 | "opencv-python": "Wrapper package for OpenCV python bindings.", 67 | "orjson": "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy", 68 | "packaging": "Core utilities for Python packages", 69 | "pandas": "Powerful data structures for data analysis, time series, and statistics", 70 | "pillow": "Python Imaging Library (Fork)", 71 | "pip": "The PyPA recommended tool for installing Python packages.", 72 | "platformdirs": "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`.", 73 | "propcache": "Accelerated property cache", 74 | "protobuf": null, 75 | "psutil": "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7.", 76 | "pyarrow": "Python library for Apache Arrow", 77 | "pycparser": "C parser in Python", 78 | "pydantic": "Data validation using Python type hints", 79 | "pydantic_core": "Core functionality for Pydantic validation and serialization", 80 | "pydub": "Manipulate audio with an simple and easy high level interface", 81 | "pygments": "Pygments is a syntax highlighting package written in Python.", 82 | "pymupdf": "A high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents.", 83 | "pyparsing": "pyparsing module - Classes and methods to define and execute parsing grammars", 84 | "pyreadline3": "A python implementation of GNU readline.", 85 | "python-dateutil": "Extensions to the standard Python datetime module", 86 | "python-dotenv": "Read key-value pairs from a .env file and set them as environment variables", 87 | "python-multipart": "A streaming multipart parser for Python", 88 | "pytz": "World timezone definitions, modern and historical", 89 | "pywin32": "Python for Window Extensions", 90 | "pyyaml": "YAML parser and emitter for Python", 91 | "regex": "Alternative regular expression module, to replace re.", 92 | "requests": "Python HTTP for Humans.", 93 | "rich": "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal", 94 | "ruff": "An extremely fast Python linter and code formatter, written in Rust.", 95 | "safehttpx": "A small Python library created to help developers protect their applications from Server Side Request Forgery (SSRF) attacks.", 96 | "safetensors": null, 97 | "scikit-learn": "A set of python modules for machine learning and data mining", 98 | "scipy": "Fundamental algorithms for scientific computing in Python", 99 | "semantic-version": "A library implementing the 'SemVer' scheme.", 100 | "sentencepiece": "SentencePiece python wrapper", 101 | "setuptools": "Easily download, build, install, upgrade, and uninstall Python packages", 102 | "shellingham": "Tool to Detect Surrounding Shell", 103 | "six": "Python 2 and 3 compatibility utilities", 104 | "sniffio": "Sniff out which async library your code is running under", 105 | "soupsieve": "A modern CSS selector implementation for Beautiful Soup.", 106 | "starlette": "The little ASGI library that shines.", 107 | "sympy": "Computer algebra system (CAS) in Python", 108 | "tokenizers": null, 109 | "tomlkit": "Style preserving TOML library", 110 | "torch": "Tensors and Dynamic neural networks in Python with strong GPU acceleration", 111 | "torchaudio": "An audio package for PyTorch", 112 | "torchvision": "image and video datasets and models for torch deep learning", 113 | "tqdm": "Fast, Extensible Progress Meter", 114 | "transformers": "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow", 115 | "typer": "Typer, build great CLIs. Easy to code. Based on Python type hints.", 116 | "antlr4-python3-runtime": "ANTLR 4.13.2 runtime for Python 3", 117 | "av": "Pythonic bindings for FFmpeg's libraries.", 118 | "absl-py": "Abseil Python Common Libraries, see https://github.com/abseil/abseil-py.", 119 | "addict": "Addict is a dictionary whose items can be set using both attribute and item syntax.", 120 | "aggdraw": "High quality drawing interface for PIL.", 121 | "aiohappyeyeballs": "Happy Eyeballs for asyncio", 122 | "aiohttp": "Async http client/server framework (asyncio)", 123 | "aiosignal": "aiosignal: a list of registered asynchronous callbacks", 124 | "albucore": "High-performance image processing functions for deep learning and computer vision.", 125 | "albumentations": "Fast, flexible, and advanced augmentation library for deep learning, computer vision, and medical imaging. Albumentations offers a wide range of transformations for both 2D (images, masks, bboxes, keypoints) and 3D (volumes, volumetric masks, keypoints) data, with optimized performance and seamless integration into ML workflows.", 126 | "altair": "Vega-Altair: A declarative statistical visualization library for Python.", 127 | "audioread": "Multi-library, cross-platform audio decoding.", 128 | "bert-score": "PyTorch implementation of BERT score", 129 | "bitsandbytes": "k-bit optimizers and matrix multiplication routines.", 130 | "bizyengine": "[a/BizyAir](https://github.com/siliconflow/BizyAir) Comfy Nodes that can run in any environment.", 131 | "bizyui": "Web resources for [a/BizyAir](https://github.com/siliconflow/BizyAir) which contains Comfy Nodes that can run in any environment.", 132 | "blend_modes": "Image processing blend modes", 133 | "blind-watermark": "Blind Watermark in Python", 134 | "blis": "The Blis BLAS-like linear algebra library, as a self-contained C-extension.", 135 | "braceexpand": "Bash-style brace expansion for Python", 136 | "cachetools": "Extensible memoizing collections and decorators", 137 | "catalogue": "Super lightweight function registries for your library", 138 | "clip": "A CLI clipboard manager", 139 | "clip-interrogator": "Generate a prompt from an image", 140 | "cloudpathlib": "pathlib-style classes for cloud storage services.", 141 | "colorlog": "Add colours to the output of Python's logging module.", 142 | "color-matcher": "Package enabling color transfer across images", 143 | "colour-science": "Colour Science for Python", 144 | "comfyui_frontend_package": null, 145 | "comfyui_workflow_templates": "ComfyUI workflow templates package", 146 | "confection": "The sweetest config system for Python", 147 | "contourpy": "Python library for calculating contours of 2D quadrilateral grids", 148 | "cssselect2": "CSS selectors for Python ElementTree", 149 | "cupy-cuda12x": "CuPy: NumPy & SciPy for GPU", 150 | "cymem": "Manage calls to calloc/free through Cython", 151 | "cython": "The Cython compiler for writing C extensions in the Python language.", 152 | "ddt": "Data-Driven/Decorated Tests", 153 | "decorator": "Decorators for Humans", 154 | "deepdiff": "Deep Difference and Search of any Python object/data. Recreate objects by adding adding deltas to each other.", 155 | "deprecated": "Python @deprecated decorator to deprecate old python classes, functions or methods.", 156 | "dill": "serialize all of Python", 157 | "diskcache": "Disk Cache -- Disk and file backed persistent cache.", 158 | "dlib": "A toolkit for making real world machine learning and data analysis applications", 159 | "docopt": "Pythonic argument parser, that will make you smile", 160 | "docstring_parser": "Parse Python docstrings in reST, Google and Numpydoc format", 161 | "docutils": "Docutils -- Python Documentation Utilities", 162 | "easydict": "Access dict values as attributes (works recursively).", 163 | "eval_type_backport": "Like `typing._eval_type`, but lets older Python versions use newer typing features.", 164 | "facexlib": "Basic face library", 165 | "fairscale": "FairScale: A PyTorch library for large-scale and high-performance training.", 166 | "fal_client": "Python client for fal.ai", 167 | "fastrlock": "Fast, re-entrant optimistic lock implemented in Cython", 168 | "filterpy": "Kalman filtering and optimal estimation library", 169 | "flet": "Flet for Python - easily build interactive multi-platform apps in Python", 170 | "freetype-py": "Freetype python bindings", 171 | "ftfy": "Fixes mojibake and other problems with Unicode, after the fact", 172 | "fvcore": "Collection of common code shared among different research projects in FAIR computer vision team", 173 | "gdown": "Google Drive Public File/Folder Downloader", 174 | "gguf": "Read and write ML models in GGUF for GGML", 175 | "gitdb": "Git Object Database", 176 | "gitpython": "GitPython is a Python library used to interact with Git repositories", 177 | "glfw": "A ctypes-based wrapper for GLFW3.", 178 | "glitch_this": "A package to glitch images and GIFs, with highly customizable options!", 179 | "googleapis-common-protos": "Common protobufs used in Google APIs", 180 | "google-ai-generativelanguage": "Google Ai Generativelanguage API client library", 181 | "google-api-core": "Google API client core library", 182 | "google-api-python-client": "Google API Client Library for Python", 183 | "google-auth": "Google Authentication Library", 184 | "google-auth-httplib2": "Google Authentication Library: httplib2 transport", 185 | "google-cloud-core": "Google Cloud API client core library", 186 | "google-cloud-storage": "Google Cloud Storage API client library", 187 | "google-crc32c": "A python wrapper of the C library 'Google CRC32C'", 188 | "google-genai": "GenAI Python SDK", 189 | "google-generativeai": "Google Generative AI High level API client library and tools.", 190 | "google-resumable-media": "Utilities for Google Media Downloads and Resumable Uploads", 191 | "grpcio": "HTTP/2-based RPC framework", 192 | "grpcio-status": "Status proto mapping for gRPC", 193 | "h5py": "Read and write HDF5 files from Python", 194 | "httplib2": "A comprehensive HTTP client library.", 195 | "hydra-core": "A framework for elegantly configuring complex applications", 196 | "imageio": "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats.", 197 | "imageio-ffmpeg": "FFMPEG wrapper for Python", 198 | "image-reward": "ImageReward", 199 | "img2texture": "Command line utility for converting images to seamless tiles.", 200 | "inputimeout": "Multi platform standard input with timeout", 201 | "insightface": "InsightFace Python Library", 202 | "iopath": "A library for providing I/O abstraction.", 203 | "jax": "Differentiate, compile, and transform Numpy code.", 204 | "jaxlib": "XLA library for JAX", 205 | "jsonschema": "An implementation of JSON Schema validation for Python", 206 | "jsonschema-specifications": "The JSON Schema meta-schemas and vocabularies, exposed as a Registry", 207 | "kornia": "Open Source Differentiable Computer Vision Library for PyTorch", 208 | "kornia_rs": "Low level implementations for computer vision in Rust", 209 | "langcodes": "Tools for labeling human languages with IETF language tags", 210 | "language_data": "Supplementary data about languages used by the langcodes module", 211 | "lark": "a modern parsing library", 212 | "lazy_loader": "Makes it easy to load subpackages and functions on demand.", 213 | "librosa": "Python module for audio and music processing", 214 | "lightning-utilities": "Lightning toolbox for across the our ecosystem.", 215 | "litelama": "A lightweight LAMA model inference wrapper", 216 | "llama_cpp_python": "Python bindings for the llama.cpp library", 217 | "llvmlite": "lightweight wrapper around basic LLVM functionality", 218 | "lmdb": "Universal Python binding for the LMDB 'Lightning' Database", 219 | "loguru": "Python logging made (stupidly) simple", 220 | "manifold3d": "Library for geometric robustness", 221 | "mapbox_earcut": "Python bindings for the mapbox earcut C++ polygon triangulation library", 222 | "marisa-trie": "Static memory-efficient and fast Trie-like structures for Python.", 223 | "matrix-client": "Client-Server SDK for Matrix", 224 | "mediapipe": "MediaPipe is the simplest way for researchers and developers to build world-class ML solutions and applications for mobile, edge, cloud and the web.", 225 | "ml_dtypes": null, 226 | "moviepy": "Video editing with Python", 227 | "msgpack": "MessagePack serializer", 228 | "mss": "An ultra fast cross-platform multiple screenshots module in pure python using ctypes.", 229 | "multiprocess": "better multiprocessing and multithreading in Python", 230 | "murmurhash": "Cython bindings for MurmurHash", 231 | "narwhals": "Extremely lightweight compatibility layer between dataframe libraries", 232 | "numba": "compiling Python code using LLVM", 233 | "nvidia-ml-py": "Python Bindings for the NVIDIA Management Library", 234 | "oauthlib": "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic", 235 | "omegaconf": "A flexible configuration library", 236 | "onnx": "Open Neural Network Exchange", 237 | "onnxruntime-gpu": "ONNX Runtime is a runtime accelerator for Machine Learning models", 238 | "openai": "The official Python library for the openai API", 239 | "opencv-contrib-python": "Wrapper package for OpenCV python bindings.", 240 | "open_clip_torch": "Open reproduction of consastive language-image pretraining (CLIP) and related.", 241 | "opt_einsum": "Path optimization of einsum functions.", 242 | "orderly-set": "Orderly set", 243 | "pdf2image": "A wrapper around the pdftoppm and pdftocairo command line tools to convert PDF to a PIL Image list.", 244 | "peft": "Parameter-Efficient Fine-Tuning (PEFT)", 245 | "piexif": "To simplify exif manipulations with python. Writing, reading, and more...", 246 | "pilgram": "library for instagram filters", 247 | "pixeloe": "Detail-Oriented Pixelization based on Contrast-Aware Outline Expansion.", 248 | "pooch": "A friend to fetch your data files", 249 | "portalocker": "Wraps the portalocker recipe for easy usage", 250 | "preshed": "Cython hash table that trusts the keys are pre-hashed", 251 | "prettytable": "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format", 252 | "proglog": "Log and progress bar manager for console, notebooks, web...", 253 | "proto-plus": "Beautiful, Pythonic protocol buffers", 254 | "psd-tools": "Python package for working with Adobe Photoshop PSD files", 255 | "pyasn1": "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)", 256 | "pyasn1_modules": "A collection of ASN.1-based protocols modules", 257 | "pycollada": "python library for reading and writing collada documents", 258 | "pydeck": "Widget for deck.gl maps", 259 | "pygit2": "Python bindings for libgit2.", 260 | "pygithub": "Use the full Github API v3", 261 | "pyglet": "pyglet is a cross-platform games and multimedia package.", 262 | "pyjwt": "JSON Web Token implementation in Python", 263 | "pymatting": "Python package for alpha matting.", 264 | "pynacl": "Python binding to the Networking and Cryptography (NaCl) library", 265 | "pynvml": "Python utilities for the NVIDIA Management Library", 266 | "pyopengl": "Standard OpenGL bindings for Python", 267 | "pypdf2": "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files", 268 | "pyrender": "Easy-to-use Python renderer for 3D visualization", 269 | "pysocks": "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information.", 270 | "pytorch-lightning": "PyTorch Lightning is the lightweight PyTorch wrapper for ML researchers. Scale your models. Write less boilerplate.", 271 | "pywavelets": "PyWavelets, wavelet transform module", 272 | "pyzbar": "Read one-dimensional barcodes and QR codes from Python 2 and 3.", 273 | "py-cpuinfo": "Get CPU info with pure Python", 274 | "qrcode": "QR Code image generator", 275 | "referencing": "JSON Referencing + Python", 276 | "rembg": "Remove image background", 277 | "repath": "Generate regular expressions form ExpressJS path patterns", 278 | "reportlab": "The Reportlab Toolkit", 279 | "requirements-parser": "This is a small Python module for parsing Pip requirement files.", 280 | "rich-argparse": "Rich help formatters for argparse and optparse", 281 | "rpds-py": "Python bindings to Rust's persistent data structures (rpds)", 282 | "rsa": "Pure-Python RSA implementation", 283 | "rtree": "R-Tree spatial index for Python GIS", 284 | "runwayml": "The official Python library for the runwayml API", 285 | "sageattention": "Accurate and efficient 8-bit plug-and-play attention.", 286 | "scikit-image": "Image processing in Python", 287 | "seaborn": "Statistical data visualization", 288 | "segment-anything": "", 289 | "shapely": "Manipulation and analysis of geometric objects", 290 | "shtab": "Automagic shell tab completion for Python CLI applications", 291 | "simpleeval": "A simple, safe single expression evaluator library.", 292 | "simsimd": "Portable mixed-precision BLAS-like vector math library for x86 and ARM", 293 | "smart-open": "Utils for streaming large files (S3, HDFS, GCS, Azure Blob Storage, gzip, bz2...)", 294 | "smmap": "A pure Python implementation of a sliding window memory map manager", 295 | "smplx": "PyTorch module for loading the SMPLX body model", 296 | "sounddevice": "Play and Record Sound with Python", 297 | "soundfile": "An audio library based on libsndfile, CFFI and NumPy", 298 | "soxr": "High quality, one-dimensional sample-rate conversion library", 299 | "spacy": "Industrial-strength Natural Language Processing (NLP) in Python", 300 | "spacy-legacy": "Legacy registered functions for spaCy backwards compatibility", 301 | "spacy-loggers": "Logging utilities for SpaCy", 302 | "spandrel": "Give your project support for a variety of PyTorch model architectures, including auto-detecting model architecture from just .pth files. spandrel gives you arch support.", 303 | "srsly": "Modern high-performance serialization utilities for Python", 304 | "streamlit": "A faster way to build and share data apps", 305 | "stringzilla": "SIMD-accelerated string search, sort, hashes, fingerprints, & edit distances", 306 | "svg.path": "SVG path objects and parser", 307 | "svglib": "A pure-Python library for reading and converting SVG", 308 | "tabulate": "Pretty-print tabular data", 309 | "tenacity": "Retry code until it succeeds", 310 | "termcolor": "ANSI color formatting for output in terminal", 311 | "thinc": "A refreshing functional take on deep learning, compatible with your favorite libraries", 312 | "threadpoolctl": "threadpoolctl", 313 | "tifffile": "Read and write TIFF files", 314 | "timm": "PyTorch Image Models", 315 | "tinycss2": "A tiny CSS parser", 316 | "tipo-kgen": "TIPO: Text to Image with text Presampling for Optimal prompting", 317 | "toml": "Python Library for Tom's Obvious, Minimal Language", 318 | "torchmetrics": "PyTorch native Metrics", 319 | "torchscale": "Transformers at any scale", 320 | "torchsde": "SDE solvers and stochastic adjoint sensitivity analysis in PyTorch.", 321 | "tornado": "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed.", 322 | "trampoline": "Simple and tiny yield-based trampoline implementation.", 323 | "transparent-background": "Make images with transparent background", 324 | "trimesh": "Import, export, process, analyze and view triangular meshes.", 325 | "triton-windows": "A language and compiler for custom Deep Learning operations", 326 | "typer-config": "Utilities for working with configuration files in typer CLIs. ", 327 | "types-setuptools": "Typing stubs for setuptools", 328 | "tyro": "CLI interfaces & config objects, from types", 329 | "ultralytics": "Ultralytics YOLO 🚀 for SOTA object detection, multi-object tracking, instance segmentation, pose estimation and image classification.", 330 | "ultralytics-thop": "Ultralytics THOP package for fast computation of PyTorch model FLOPs and parameters.", 331 | "uritemplate": "Implementation of RFC 6570 URI Templates", 332 | "uv": "An extremely fast Python package and project manager, written in Rust.", 333 | "vhacdx": "Python bindings for VHACD", 334 | "wand": "Ctypes-based simple MagickWand API binding for Python", 335 | "wasabi": "A lightweight console printing and formatting toolkit", 336 | "watchdog": "Filesystem events monitoring", 337 | "wcwidth": "Measures the displayed width of unicode strings in a terminal", 338 | "weasel": "Weasel: A small and easy workflow system", 339 | "webdataset": "High performance storage and I/O for deep learning and data processing.", 340 | "win32_setctime": "A small Python utility to set file creation time on Windows", 341 | "wrapt": "Module for decorators, wrappers and monkey patching.", 342 | "xformers": "XFormers: A collection of composable Transformer building blocks.", 343 | "xxhash": "Python binding for xxHash", 344 | "yacs": "Yet Another Configuration System", 345 | "yapf": "A formatter for Python code", 346 | "yarl": "Yet another URL library", 347 | "zhipuai": "A SDK library for accessing big model apis from ZhipuAI", 348 | "asyncio": "reference implementation of PEP 3156", 349 | "bottle": "Fast and simple WSGI-framework for small web-applications.", 350 | "clr_loader": "Generic pure Python loader for .NET runtimes", 351 | "customtkinter": "Create modern looking GUIs with Python", 352 | "darkdetect": "Detect OS Dark Mode from Python", 353 | "ebooklib": "Ebook library which can handle EPUB2/EPUB3 and Kindle format", 354 | "elasticsearch": "Python client for Elasticsearch", 355 | "et_xmlfile": "An implementation of lxml.xmlfile for the standard library", 356 | "flash_attn": "Flash Attention: Fast and Memory-Efficient Exact Attention", 357 | "flask": "A simple framework for building complex web applications.", 358 | "flask-cors": "A Flask extension simplifying CORS support", 359 | "iniconfig": "brain-dead simple config-ini parsing", 360 | "ipex-llm": "Large Language Model Develop Toolkit", 361 | "itsdangerous": "Safely pass data to untrusted environments and back.", 362 | "langchain": "Building applications with LLMs through composability", 363 | "lit": "A Software Testing Tool", 364 | "mcp": "Model Context Protocol SDK", 365 | "mcp-server-time": "A Model Context Protocol server providing tools for time queries and timezone conversions for LLMs", 366 | "ninja": "Ninja is a small build system with a focus on speed", 367 | "nltk": "Natural Language Toolkit", 368 | "opencv-python-headless": "Wrapper package for OpenCV python bindings.", 369 | "openpyxl": "A Python library to read/write Excel 2010 xlsx/xlsm files", 370 | "pefile": "Python PE parsing module", 371 | "pipdeptree": "Command line utility to show dependency tree of packages.", 372 | "pluggy": "plugin and hook calling mechanisms for python", 373 | "prometheus_client": "Python client for the Prometheus monitoring system.", 374 | "prov": "A library for W3C Provenance Data Model supporting PROV-JSON, PROV-XML and PROV-O (RDF)", 375 | "proxy_tools": "Proxy Implementation", 376 | "pybind11": "Seamless operability between C++11 and Python", 377 | "pydantic-settings": "Settings management using Pydantic", 378 | "pyinstaller": "PyInstaller bundles a Python application and all its dependencies into a single package.", 379 | "pyinstaller-hooks-contrib": "Community maintained hooks for PyInstaller", 380 | "pymongo": "PyMongo - the Official MongoDB Python driver", 381 | "pymysql": "Pure Python MySQL Driver", 382 | "pytesseract": "Python-tesseract is a python wrapper for Google's Tesseract-OCR", 383 | "pytest": "pytest: simple powerful testing with Python", 384 | "pythonnet": ".NET and Mono integration for Python", 385 | "python-docx": "Create, read, and update Microsoft Word .docx files.", 386 | "pywebview": "Build GUI for your Python program with JavaScript, HTML, and CSS", 387 | "pywin32-ctypes": "A (partial) reimplementation of pywin32 using ctypes/cffi", 388 | "redis": "Python client for Redis database and key-value store", 389 | "sentence-transformers": "Embeddings, Retrieval, and Reranking", 390 | "sqlalchemy": "Database Abstraction Library", 391 | "sse-starlette": "SSE plugin for Starlette", 392 | "werkzeug": "The comprehensive WSGI web application library.", 393 | "wheel": "A built-package format for Python", 394 | "xlrd": "Library for developers to extract data from Microsoft Excel (tm) .xls spreadsheet files", 395 | "altgraph": "Python graph (network) package" 396 | } -------------------------------------------------------------------------------- /config/python_environments.json: -------------------------------------------------------------------------------- 1 | { 2 | "environments": [ 3 | { 4 | "id": "system", 5 | "name": "系统环境", 6 | "path": "C:\\Users\\Dontdrunk\\AppData\\Local\\Programs\\Python\\Python312\\python.exe", 7 | "type": "system", 8 | "version": "3.12.9" 9 | }, 10 | { 11 | "id": "4e4d80b2-828f-40ce-ad17-dd2ea787e963", 12 | "name": "ComfyUI环境", 13 | "path": "D:\\Projects\\Procedure\\ComfyUI-X\\python_embeded\\python.exe", 14 | "type": "custom", 15 | "version": "3.12.9" 16 | } 17 | ], 18 | "current": "system" 19 | } -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | /* 主CSS文件 - 仅用于导入其他模块 */ 2 | 3 | /* 基础样式 */ 4 | @import 'modules/base.css'; 5 | 6 | /* 功能组件 */ 7 | @import 'modules/components.css'; 8 | 9 | /* 工具与动画 */ 10 | @import 'modules/utilities.css'; 11 | -------------------------------------------------------------------------------- /css/modules/base.css: -------------------------------------------------------------------------------- 1 | /* Base.css - 基础核心模块 */ 2 | 3 | /* ====================================================== 4 | 设计系统变量 - 全局变量定义 5 | ====================================================== */ 6 | :root { 7 | /* 扩展色板 - 主色 */ 8 | --primary-100: #e3f2fd; 9 | --primary-200: #bbdefb; 10 | --primary-300: #90caf9; 11 | --primary-400: #64b5f6; 12 | --primary-500: #42a5f5; 13 | --primary-600: #2196f3; 14 | --primary-700: #1e88e5; 15 | --primary-800: #1976d2; 16 | --primary-900: #1565c0; 17 | 18 | /* 中性色 */ 19 | --neutral-50: #fafafa; 20 | --neutral-100: #f5f5f5; 21 | --neutral-200: #eeeeee; 22 | --neutral-300: #e0e0e0; 23 | --neutral-400: #bdbdbd; 24 | --neutral-500: #9e9e9e; 25 | --neutral-600: #757575; 26 | --neutral-700: #616161; 27 | --neutral-800: #424242; 28 | --neutral-900: #212121; 29 | 30 | /* 功能色 */ 31 | --success-color: #00b894; 32 | --warning-color: #f39c12; 33 | --error-color: #e74c3c; 34 | --info-color: #3498db; 35 | 36 | /* 应用变量 */ 37 | --primary-color: var(--primary-600); 38 | --primary-light: var(--primary-400); 39 | --primary-dark: var(--primary-800); 40 | --secondary-color: var(--primary-800); 41 | 42 | --background-color: var(--neutral-100); 43 | --card-color: #ffffff; 44 | --text-color: var(--neutral-800); 45 | --secondary-text: var(--neutral-600); 46 | --border-color: var(--neutral-300); 47 | 48 | /* 阴影 */ 49 | --shadow-sm: 0 1px 2px rgba(0,0,0,0.05); 50 | --shadow-md: 0 4px 6px rgba(0,0,0,0.07); 51 | --shadow-lg: 0 10px 15px rgba(0,0,0,0.1); 52 | 53 | /* 间距 */ 54 | --space-xs: 4px; 55 | --space-sm: 8px; 56 | --space-md: 16px; 57 | --space-lg: 24px; 58 | --space-xl: 32px; 59 | 60 | /* 边框圆角 */ 61 | --radius-sm: 4px; 62 | --radius-md: 8px; 63 | --radius-lg: 12px; 64 | --radius-xl: 20px; 65 | 66 | /* 排版 */ 67 | --font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, system-ui, Roboto, 'Helvetica Neue', sans-serif; 68 | } 69 | 70 | /* ====================================================== 71 | 基础元素样式 72 | ====================================================== */ 73 | * { 74 | margin: 0; 75 | padding: 0; 76 | box-sizing: border-box; 77 | } 78 | 79 | body { 80 | font-family: var(--font-family); 81 | background-color: var(--background-color); 82 | color: var(--text-color); 83 | line-height: 1.6; 84 | font-size: 16px; 85 | } 86 | 87 | .container { 88 | max-width: 1200px; 89 | margin: 0 auto; 90 | padding: var(--space-lg); 91 | } 92 | 93 | /* 标题样式 */ 94 | h1, h2, h3 { 95 | color: var(--primary-color); 96 | font-weight: 600; 97 | } 98 | 99 | h1 { 100 | margin-right: var(--space-lg); 101 | font-size: 1.75rem; 102 | } 103 | 104 | h2 { 105 | font-size: 1.5rem; 106 | margin-bottom: var(--space-md); 107 | } 108 | 109 | h3 { 110 | font-size: 1.25rem; 111 | margin-bottom: var(--space-sm); 112 | } 113 | 114 | /* 按钮基础样式 */ 115 | button { 116 | background-color: var(--primary-color); 117 | color: white; 118 | border: none; 119 | padding: 8px 16px; 120 | border-radius: var(--radius-sm); 121 | font-weight: 500; 122 | cursor: pointer; 123 | transition: all 0.2s ease; 124 | font-size: 0.9rem; 125 | display: inline-flex; 126 | align-items: center; 127 | justify-content: center; 128 | gap: var(--space-xs); 129 | } 130 | 131 | button:hover { 132 | background-color: var(--primary-dark); 133 | transform: translateY(-1px); 134 | box-shadow: var(--shadow-sm); 135 | } 136 | 137 | button:active { 138 | transform: translateY(0); 139 | } 140 | 141 | button:disabled { 142 | opacity: 0.6; 143 | cursor: not-allowed; 144 | transform: none; 145 | box-shadow: none; 146 | } 147 | 148 | /* 表单元素基础样式 */ 149 | input[type="text"], 150 | input[type="file"], 151 | select { 152 | padding: 10px 12px; 153 | border: 1px solid var(--border-color); 154 | border-radius: var(--radius-sm); 155 | font-family: var(--font-family); 156 | font-size: 0.95rem; 157 | transition: all 0.2s; 158 | } 159 | 160 | input[type="text"]:focus, 161 | select:focus { 162 | outline: none; 163 | border-color: var(--primary-color); 164 | box-shadow: 0 0 0 3px var(--primary-100); 165 | } 166 | 167 | /* ====================================================== 168 | 布局组件 169 | ====================================================== */ 170 | /* 头部样式 */ 171 | header { 172 | display: flex; 173 | flex-wrap: wrap; 174 | justify-content: space-between; 175 | align-items: center; 176 | margin-bottom: var(--space-xl); 177 | padding-bottom: var(--space-md); 178 | border-bottom: 1px solid var(--border-color); 179 | } 180 | 181 | /* 状态栏样式 */ 182 | .status-bar { 183 | display: flex; 184 | justify-content: space-between; 185 | align-items: center; 186 | font-size: 0.875rem; 187 | color: var(--secondary-text); 188 | margin-left: auto; 189 | padding-left: var(--space-lg); 190 | } 191 | 192 | .system-info { 193 | display: flex; 194 | gap: var(--space-lg); 195 | align-items: center; 196 | } 197 | 198 | /* 作者链接样式 */ 199 | .author-links { 200 | display: flex; 201 | align-items: center; 202 | gap: var(--space-sm); 203 | } 204 | 205 | .author-btn { 206 | background: transparent; 207 | border: none; 208 | color: var(--secondary-text); 209 | padding: var(--space-xs); 210 | cursor: pointer; 211 | border-radius: 50%; 212 | width: 36px; 213 | height: 36px; 214 | display: flex; 215 | align-items: center; 216 | justify-content: center; 217 | transition: all 0.2s; 218 | } 219 | 220 | .author-btn:hover { 221 | background-color: var(--neutral-200); 222 | color: var(--primary-color); 223 | transform: scale(1.1); 224 | } 225 | 226 | /* 页脚样式 */ 227 | footer { 228 | margin-top: var(--space-xl); 229 | padding-top: var(--space-lg); 230 | text-align: center; 231 | font-size: 0.875rem; 232 | color: var(--secondary-text); 233 | border-top: 1px solid var(--border-color); 234 | } 235 | 236 | /* ====================================================== 237 | 通知和加载状态 238 | ====================================================== */ 239 | /* 通知样式 */ 240 | .notification { 241 | position: fixed; 242 | top: var(--space-lg); 243 | right: var(--space-lg); 244 | padding: var(--space-md) var(--space-lg); 245 | border-radius: var(--radius-md); 246 | background-color: white; 247 | color: var(--text-primary); 248 | font-weight: 500; 249 | box-shadow: var(--shadow-md); 250 | z-index: 1000; 251 | opacity: 0; 252 | transform: translateY(-20px); 253 | transition: all 0.3s ease; 254 | max-width: 350px; 255 | border-left: 4px solid var(--primary-color); 256 | display: flex; 257 | align-items: center; 258 | gap: 0.75rem; 259 | } 260 | 261 | .notification.success { 262 | border-left-color: var(--success-color); 263 | } 264 | 265 | .notification.error { 266 | border-left-color: var(--error-color); 267 | } 268 | 269 | .notification.warning { 270 | border-left-color: var(--warning-color); 271 | } 272 | 273 | .notification.show { 274 | opacity: 1; 275 | transform: translateY(0); 276 | } 277 | 278 | .notification.hidden { 279 | display: none; 280 | } 281 | 282 | .notification-icon { 283 | display: flex; 284 | align-items: center; 285 | justify-content: center; 286 | width: 20px; 287 | height: 20px; 288 | flex-shrink: 0; 289 | } 290 | 291 | .notification-content { 292 | flex: 1; 293 | } 294 | 295 | .notification-close { 296 | background: none; 297 | border: none; 298 | color: var(--text-secondary); 299 | cursor: pointer; 300 | padding: 4px; 301 | border-radius: 50%; 302 | display: flex; 303 | align-items: center; 304 | justify-content: center; 305 | transition: background-color 0.2s; 306 | } 307 | 308 | .notification-close:hover { 309 | background-color: rgba(0, 0, 0, 0.05); 310 | color: var(--text-primary); 311 | } 312 | 313 | /* 加载状态样式 */ 314 | .loading { 315 | text-align: center; 316 | padding: var(--space-xl); 317 | color: var(--secondary-text); 318 | font-style: italic; 319 | } 320 | 321 | /* ====================================================== 322 | 模态框基础样式 323 | ====================================================== */ 324 | .modal { 325 | display: none; 326 | position: fixed; 327 | top: 0; 328 | left: 0; 329 | right: 0; 330 | bottom: 0; 331 | width: 100%; 332 | height: 100%; 333 | background-color: rgba(0, 0, 0, 0.5); 334 | z-index: 1000; 335 | align-items: center; 336 | justify-content: center; 337 | overflow-y: auto; 338 | padding: var(--space-lg) 0; 339 | } 340 | 341 | .modal-content { 342 | background-color: var(--card-color); 343 | margin: auto; 344 | padding: var(--space-xl); 345 | border-radius: var(--radius-lg); 346 | width: 500px; 347 | max-width: 95%; 348 | max-height: 90vh; 349 | box-shadow: var(--shadow-lg); 350 | position: relative; 351 | overflow-y: auto; 352 | animation: modalFadeIn 0.3s ease; 353 | } 354 | 355 | @keyframes modalFadeIn { 356 | from { opacity: 0; transform: translateY(-20px); } 357 | to { opacity: 1; transform: translateY(0); } 358 | } 359 | 360 | .close-btn { 361 | position: absolute; 362 | top: var(--space-md); 363 | right: var(--space-md); 364 | font-size: 1.5rem; 365 | font-weight: bold; 366 | cursor: pointer; 367 | color: var(--neutral-500); 368 | transition: color 0.2s; 369 | width: 32px; 370 | height: 32px; 371 | display: flex; 372 | align-items: center; 373 | justify-content: center; 374 | border-radius: 50%; 375 | } 376 | 377 | .close-btn:hover { 378 | color: var(--error-color); 379 | background-color: var(--neutral-100); 380 | } 381 | 382 | /* 错误详情模态框样式 */ 383 | .error-modal-content { 384 | width: 600px; 385 | max-width: 90%; 386 | max-height: 80vh; 387 | } 388 | 389 | .error-list { 390 | max-height: 300px; 391 | overflow-y: auto; 392 | margin: 15px 0; 393 | border: 1px solid var(--border-color); 394 | border-radius: 4px; 395 | } 396 | 397 | .error-item { 398 | padding: 10px; 399 | border-bottom: 1px solid var(--border-color); 400 | } 401 | 402 | .error-item:last-child { 403 | border-bottom: none; 404 | } 405 | 406 | .error-package { 407 | font-weight: bold; 408 | color: var(--error-color); 409 | margin-bottom: 5px; 410 | } 411 | 412 | .error-message { 413 | margin-bottom: 5px; 414 | } 415 | 416 | .error-details { 417 | font-size: 0.9em; 418 | background-color: #f5f5f5; 419 | padding: 8px; 420 | border-radius: 4px; 421 | white-space: pre-wrap; 422 | overflow-x: auto; 423 | color: #666; 424 | } 425 | 426 | .modal-footer { 427 | display: flex; 428 | justify-content: flex-end; 429 | margin-top: 15px; 430 | } 431 | 432 | /* ====================================================== 433 | 初始加载界面 434 | ====================================================== */ 435 | #initial-loading-screen { 436 | position: fixed; 437 | top: 0; 438 | left: 0; 439 | width: 100%; 440 | height: 100%; 441 | background-color: var(--background-color); 442 | z-index: 10000; 443 | display: flex; 444 | justify-content: center; 445 | align-items: center; 446 | transition: opacity 0.5s ease; 447 | } 448 | 449 | #initial-loading-screen.fade-out { 450 | opacity: 0; 451 | } 452 | 453 | .initial-loading-content { 454 | background-color: var(--card-color); 455 | border-radius: var(--radius-lg); 456 | box-shadow: var(--shadow-lg); 457 | padding: var(--space-xl); 458 | width: 90%; 459 | max-width: 500px; 460 | text-align: center; 461 | } 462 | 463 | .initial-loading-content h2 { 464 | color: var(--primary-color); 465 | margin-bottom: var(--space-lg); 466 | } 467 | 468 | .initial-progress-container { 469 | height: 10px; 470 | background-color: var(--neutral-200); 471 | border-radius: 5px; 472 | margin-bottom: var(--space-sm); 473 | overflow: hidden; 474 | } 475 | 476 | .initial-progress-bar { 477 | height: 100%; 478 | width: 0; 479 | background-color: var(--primary-color); 480 | transition: width 0.3s ease; 481 | border-radius: 5px; 482 | } 483 | 484 | .initial-progress-text { 485 | font-size: 0.875rem; 486 | color: var(--secondary-text); 487 | font-weight: 500; 488 | margin-bottom: var(--space-md); 489 | } 490 | 491 | .initial-loading-info { 492 | color: var(--secondary-text); 493 | font-size: 0.9rem; 494 | margin-bottom: var(--space-lg); 495 | } 496 | -------------------------------------------------------------------------------- /css/modules/components.css: -------------------------------------------------------------------------------- 1 | /* Components.css - 功能组件模块 */ 2 | 3 | /* ====================================================== 4 | 依赖管理组件 5 | ====================================================== */ 6 | 7 | /* 依赖容器卡片样式 */ 8 | .dependency-container { 9 | background-color: var(--card-color); 10 | border-radius: var(--radius-lg); 11 | box-shadow: var(--shadow-md); 12 | margin-bottom: var(--space-xl); 13 | overflow: hidden; 14 | border: 1px solid var(--border-color); 15 | } 16 | 17 | /* 批量操作面板 */ 18 | .batch-actions { 19 | display: flex; 20 | flex-wrap: wrap; 21 | justify-content: space-between; 22 | align-items: center; 23 | padding: var(--space-md) var(--space-lg); 24 | background-color: var(--neutral-50); 25 | border-bottom: 1px solid var(--border-color); 26 | gap: var(--space-md); 27 | } 28 | 29 | .batch-left { 30 | display: flex; 31 | align-items: center; 32 | gap: var(--space-md); 33 | flex-wrap: wrap; 34 | } 35 | 36 | /* 筛选器容器 */ 37 | .filter-container { 38 | display: flex; 39 | align-items: center; 40 | position: relative; 41 | } 42 | 43 | #dependency-filter { 44 | padding: 8px 12px; 45 | border-radius: var(--radius-sm); 46 | border: 1px solid var(--border-color); 47 | font-size: 0.875rem; 48 | background-color: var(--card-color); 49 | min-width: 120px; 50 | height: 38px; 51 | appearance: none; 52 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23757575' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); 53 | background-repeat: no-repeat; 54 | background-position: right 8px center; 55 | padding-right: 32px; 56 | } 57 | 58 | /* 清理缓存按钮 */ 59 | #clean-cache-btn { 60 | background-color: var(--warning-color); 61 | height: 38px; 62 | } 63 | 64 | #clean-cache-btn:hover { 65 | background-color: #d35400; 66 | } 67 | 68 | /* 全选容器 */ 69 | .select-all-container { 70 | display: flex; 71 | align-items: center; 72 | margin-right: var(--space-md); 73 | padding: var(--space-xs) var(--space-sm); 74 | background-color: var(--neutral-100); 75 | border-radius: var(--radius-sm); 76 | border: 1px solid var(--border-color); 77 | height: 38px; 78 | } 79 | 80 | .select-all-container input[type="checkbox"] { 81 | margin-right: var(--space-sm); 82 | width: 18px; 83 | height: 18px; 84 | cursor: pointer; 85 | } 86 | 87 | .select-all-container label { 88 | cursor: pointer; 89 | user-select: none; 90 | font-weight: 500; 91 | font-size: 0.9rem; 92 | } 93 | 94 | /* 批量按钮 */ 95 | .batch-buttons { 96 | display: flex; 97 | flex-wrap: wrap; 98 | gap: var(--space-sm); 99 | align-items: center; 100 | } 101 | 102 | .batch-btn { 103 | font-size: 0.875rem; 104 | padding: 8px 12px; 105 | height: 38px; 106 | display: flex; 107 | align-items: center; 108 | white-space: nowrap; 109 | border-radius: var(--radius-sm); 110 | } 111 | 112 | /* 操作按钮 */ 113 | .action-btn { 114 | padding: 4px 8px; 115 | font-size: 0.75rem; 116 | border-radius: var(--radius-sm); 117 | transition: all 0.2s; 118 | margin-left: 3px; 119 | min-width: 0; 120 | width: auto; 121 | white-space: nowrap; 122 | flex-shrink: 0; 123 | } 124 | 125 | .action-btn:hover { 126 | transform: translateY(-1px); 127 | box-shadow: var(--shadow-sm); 128 | } 129 | 130 | /* 卸载按钮 - 红色 */ 131 | .action-btn.uninstall { 132 | background-color: var(--error-color); 133 | } 134 | 135 | .action-btn.uninstall:hover { 136 | background-color: #c0392b; 137 | } 138 | 139 | /* 版本切换按钮 - 蓝色 */ 140 | .action-btn.version { 141 | background-color: var(--info-color); 142 | } 143 | 144 | .action-btn.version:hover { 145 | background-color: #2980b9; 146 | } 147 | 148 | /* 更新按钮 - 黄色 */ 149 | .action-btn.update { 150 | background-color: var(--warning-color); 151 | } 152 | 153 | .action-btn.update:hover { 154 | background-color: #d35400; 155 | } 156 | 157 | /* 最新版本按钮 - 绿色 */ 158 | .action-btn.update.latest { 159 | background-color: var(--success-color); 160 | cursor: default; 161 | } 162 | 163 | .action-btn.update.latest:hover { 164 | background-color: #27ae60; 165 | transform: none; 166 | } 167 | 168 | /* 刚更新的依赖项高亮效果 */ 169 | .dependency-item.just-updated { 170 | background-color: var(--primary-100); 171 | border-left: 4px solid var(--primary-color); 172 | transition: all 0.3s ease; 173 | } 174 | 175 | .dependency-item.just-updated:hover { 176 | background-color: var(--primary-200); 177 | } 178 | 179 | /* 依赖列表头部 */ 180 | .dependency-header { 181 | display: flex; 182 | padding: var(--space-md) var(--space-lg); 183 | background-color: var(--neutral-100); 184 | font-weight: 600; 185 | font-size: 0.875rem; 186 | color: var(--secondary-text); 187 | border-bottom: 1px solid var(--border-color); 188 | letter-spacing: 0.5px; 189 | text-transform: uppercase; 190 | } 191 | 192 | /* 依赖列表滚动容器 */ 193 | .dependency-list { 194 | max-height: 450px; 195 | overflow-y: auto; 196 | scrollbar-width: thin; 197 | scrollbar-color: var(--neutral-400) transparent; 198 | } 199 | 200 | .dependency-list::-webkit-scrollbar { 201 | width: 6px; 202 | } 203 | 204 | .dependency-list::-webkit-scrollbar-track { 205 | background: transparent; 206 | } 207 | 208 | .dependency-list::-webkit-scrollbar-thumb { 209 | background-color: var(--neutral-400); 210 | border-radius: 3px; 211 | } 212 | 213 | /* 依赖项样式 */ 214 | .dependency-item { 215 | display: flex; 216 | padding: var(--space-md) var(--space-lg); 217 | border-bottom: 1px solid var(--border-color); 218 | align-items: center; 219 | transition: background-color 0.2s; 220 | } 221 | 222 | .dependency-item:hover { 223 | background-color: var(--neutral-50); 224 | } 225 | 226 | /* 依赖项动画效果 */ 227 | @keyframes updatePulse { 228 | 0% { background-color: rgba(52, 152, 219, 0.2); } 229 | 50% { background-color: rgba(52, 152, 219, 0.05); } 230 | 100% { background-color: rgba(52, 152, 219, 0); } 231 | } 232 | 233 | .dependency-item.just-updated { 234 | animation: updatePulse 3s ease-out; 235 | } 236 | 237 | /* 依赖项颜色分类 */ 238 | .dependency-item.core { 239 | background-color: rgba(52, 152, 219, 0.08); 240 | } 241 | 242 | .dependency-item.ai-model { 243 | background-color: rgba(46, 204, 113, 0.08); 244 | } 245 | 246 | .dependency-item.selected { 247 | background-color: var(--primary-100); 248 | } 249 | 250 | .dependency-item.system { 251 | background-color: rgba(153, 102, 255, 0.08); 252 | } 253 | 254 | .dependency-item.app-required { 255 | background-color: rgba(233, 30, 99, 0.08); 256 | } 257 | 258 | /* 依赖项淡出效果 */ 259 | .dependency-item.fading-out { 260 | opacity: 0.5; 261 | transition: opacity 0.5s; 262 | } 263 | 264 | /* 刷新中的依赖项 */ 265 | .dependency-item.refreshing { 266 | background-color: rgba(255, 255, 204, 0.3); 267 | transition: background-color 0.5s; 268 | } 269 | 270 | /* 依赖项列宽 */ 271 | .col-checkbox { 272 | width: 30px; 273 | display: flex; 274 | align-items: center; 275 | justify-content: center; 276 | } 277 | 278 | .col-checkbox input[type="checkbox"] { 279 | width: 18px; 280 | height: 18px; 281 | cursor: pointer; 282 | } 283 | 284 | .col-name { 285 | width: 20%; 286 | font-weight: 500; 287 | overflow: hidden; 288 | text-overflow: ellipsis; 289 | padding-right: var(--space-md); 290 | } 291 | 292 | .col-version { 293 | width: 10%; 294 | color: var(--secondary-text); 295 | font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; 296 | font-size: 0.875rem; 297 | } 298 | 299 | .col-description { 300 | width: 45%; 301 | color: var(--secondary-text); 302 | overflow: hidden; 303 | text-overflow: ellipsis; 304 | font-size: 0.9rem; 305 | padding-right: var(--space-md); 306 | } 307 | 308 | .col-actions { 309 | width: 25%; 310 | display: flex; 311 | justify-content: flex-end; 312 | flex-wrap: nowrap; 313 | gap: 2px; 314 | } 315 | 316 | /* 标签样式 */ 317 | .tag { 318 | display: inline-block; 319 | padding: 2px 6px; 320 | border-radius: var(--radius-sm); 321 | font-size: 0.75rem; 322 | font-weight: 500; 323 | margin-left: 8px; 324 | } 325 | 326 | .tag.core { 327 | background-color: #3498db; 328 | color: white; 329 | } 330 | 331 | .tag.ai-model { 332 | background-color: #2ecc71; 333 | color: white; 334 | } 335 | 336 | .tag.system { 337 | background-color: #9966ff; 338 | color: white; 339 | } 340 | 341 | .tag.app-required { 342 | background-color: #e91e63; 343 | color: white; 344 | } 345 | 346 | /* 依赖图按钮 */ 347 | .action-btn.dependency-graph { 348 | background-color: #9c27b0; 349 | position: relative; 350 | overflow: hidden; 351 | z-index: 1; 352 | width: auto; 353 | min-width: 0; 354 | padding: 4px 8px; 355 | } 356 | 357 | .action-btn.dependency-graph::before { 358 | content: ""; 359 | position: absolute; 360 | top: -2px; 361 | left: -2px; 362 | right: -2px; 363 | bottom: -2px; 364 | background: linear-gradient( 365 | 124deg, 366 | #ff2400, #e81d1d, #e8b71d, #e3e81d, #1de840, 367 | #1ddde8, #2b1de8, #dd00f3, #dd00f3 368 | ); 369 | background-size: 1800% 1800%; 370 | z-index: -1; 371 | animation: rainbow-pulse 6s linear infinite; 372 | border-radius: calc(var(--radius-sm) + 2px); 373 | opacity: 0.7; 374 | } 375 | 376 | @keyframes rainbow-pulse { 377 | 0% { background-position: 0% 82% } 378 | 50% { background-position: 100% 19% } 379 | 100% { background-position: 0% 82% } 380 | } 381 | 382 | .action-btn.dependency-graph:hover { 383 | transform: translateY(-1px) scale(1.05); 384 | box-shadow: 0 0 8px rgba(156, 39, 176, 0.6); 385 | } 386 | 387 | .action-btn.dependency-graph:hover::before { 388 | animation-duration: 2s; 389 | opacity: 0.9; 390 | } 391 | 392 | /* 搜索框 */ 393 | .search-container { 394 | margin-bottom: var(--space-xl); 395 | width: 100%; 396 | } 397 | 398 | .search-input-wrapper { 399 | position: relative; 400 | width: 100%; 401 | display: flex; 402 | align-items: center; 403 | } 404 | 405 | #dependency-search { 406 | width: 100%; 407 | padding: 12px 40px 12px 16px; 408 | border: 1px solid var(--border-color); 409 | border-radius: var(--radius-xl); 410 | font-size: 1rem; 411 | transition: all 0.3s; 412 | box-shadow: var(--shadow-sm); 413 | } 414 | 415 | #dependency-search:focus { 416 | outline: none; 417 | border-color: var(--primary-color); 418 | box-shadow: 0 0 0 3px var(--primary-100); 419 | } 420 | 421 | .clear-search-btn { 422 | position: absolute; 423 | right: 12px; 424 | background: transparent; 425 | border: none; 426 | color: var(--neutral-500); 427 | font-size: 20px; 428 | cursor: pointer; 429 | padding: 5px; 430 | line-height: 1; 431 | display: none; 432 | transition: color 0.2s; 433 | } 434 | 435 | .clear-search-btn:hover { 436 | color: var(--error-color); 437 | } 438 | 439 | #dependency-search:not(:placeholder-shown) + .clear-search-btn { 440 | display: block; 441 | } 442 | 443 | .highlight-match { 444 | background-color: rgba(255, 193, 7, 0.35); 445 | padding: 0 2px; 446 | border-radius: 2px; 447 | font-weight: 500; 448 | } 449 | 450 | /* ====================================================== 451 | 安装区域 452 | ====================================================== */ 453 | .package-install-container { 454 | background-color: var(--card-color); 455 | border-radius: var(--radius-lg); 456 | box-shadow: var(--shadow-md); 457 | margin-top: var(--space-xl); 458 | margin-bottom: var(--space-xl); 459 | padding: var(--space-xl); 460 | border: 1px solid var(--border-color); 461 | } 462 | 463 | .package-install-container h2 { 464 | margin-bottom: var(--space-md); 465 | font-size: 1.25rem; 466 | color: var(--primary-color); 467 | display: flex; 468 | align-items: center; 469 | gap: var(--space-sm); 470 | } 471 | 472 | .package-install-container h2::before { 473 | content: ""; 474 | display: inline-block; 475 | width: 4px; 476 | height: 18px; 477 | background-color: var(--primary-color); 478 | border-radius: 2px; 479 | } 480 | 481 | .package-input-group { 482 | display: flex; 483 | flex-wrap: wrap; 484 | gap: var(--space-sm); 485 | align-items: center; 486 | margin-bottom: var(--space-sm); 487 | } 488 | 489 | #package-input { 490 | flex: 1; 491 | min-width: 300px; 492 | padding: 10px 16px; 493 | border-radius: var(--radius-md); 494 | } 495 | 496 | .file-select-btn { 497 | background-color: var(--neutral-200); 498 | color: var(--text-color); 499 | border: 1px solid var(--border-color); 500 | white-space: nowrap; 501 | font-weight: normal; 502 | } 503 | 504 | .file-select-btn:hover { 505 | background-color: var(--neutral-300); 506 | } 507 | 508 | .install-btn { 509 | background-color: var(--success-color); 510 | color: white; 511 | white-space: nowrap; 512 | } 513 | 514 | .install-btn:hover { 515 | background-color: #27ae60; 516 | } 517 | 518 | .file-info { 519 | font-size: 0.875rem; 520 | color: var(--secondary-text); 521 | margin-top: var(--space-xs); 522 | min-height: 20px; 523 | } 524 | 525 | /* ====================================================== 526 | 进度条 527 | ====================================================== */ 528 | .progress-container { 529 | position: fixed; 530 | top: 0; 531 | left: 0; 532 | right: 0; 533 | background-color: var(--card-color); 534 | padding: var(--space-md) var(--space-lg); 535 | box-shadow: var(--shadow-md); 536 | z-index: 1100; 537 | text-align: center; 538 | border-bottom: 1px solid var(--border-color); 539 | animation: slideDown 0.3s ease; 540 | display: flex; 541 | flex-direction: column; 542 | gap: var(--space-xs); 543 | } 544 | 545 | @keyframes slideDown { 546 | from { transform: translateY(-100%); } 547 | to { transform: translateY(0); } 548 | } 549 | 550 | .progress-container.hidden { 551 | display: none; 552 | } 553 | 554 | .progress-title { 555 | font-weight: 600; 556 | margin-bottom: var(--space-md); 557 | color: var(--primary-color); 558 | } 559 | 560 | .progress-bar-container { 561 | height: 10px; 562 | background-color: var(--neutral-200); 563 | border-radius: 5px; 564 | margin-bottom: var(--space-sm); 565 | overflow: hidden; 566 | } 567 | 568 | .progress-bar { 569 | height: 100%; 570 | width: 0; 571 | background-color: var(--primary-color); 572 | transition: width 0.3s ease; 573 | border-radius: 5px; 574 | background-image: linear-gradient( 575 | 45deg, 576 | rgba(255, 255, 255, 0.15) 25%, 577 | transparent 25%, 578 | transparent 50%, 579 | rgba(255, 255, 255, 0.15) 50%, 580 | rgba(255, 255, 255, 0.15) 75%, 581 | transparent 75%, 582 | transparent 583 | ); 584 | background-size: 20px 20px; 585 | animation: progress-bar-stripes 1s linear infinite; 586 | } 587 | 588 | @keyframes progress-bar-stripes { 589 | from { background-position: 20px 0; } 590 | to { background-position: 0 0; } 591 | } 592 | 593 | .progress-text { 594 | font-size: 0.875rem; 595 | color: var(--secondary-text); 596 | font-weight: 500; 597 | } 598 | 599 | /* ====================================================== 600 | 更新确认对话框 601 | ====================================================== */ 602 | .update-confirm-content { 603 | width: 650px; 604 | max-width: 95%; 605 | max-height: 80vh; 606 | } 607 | 608 | .update-confirm-info { 609 | background-color: var(--primary-100); 610 | border-radius: var(--radius-md); 611 | padding: var(--space-md); 612 | margin-bottom: var(--space-lg); 613 | border-left: 4px solid var(--primary-color); 614 | } 615 | 616 | .update-confirm-info p { 617 | margin: 0; 618 | color: var(--primary-dark); 619 | font-weight: 500; 620 | } 621 | 622 | .update-confirm-info span { 623 | font-weight: 700; 624 | color: var(--primary-color); 625 | } 626 | 627 | .update-packages-container, 628 | .latest-packages-container { 629 | margin-bottom: var(--space-lg); 630 | } 631 | 632 | .update-packages-container h3, 633 | .latest-packages-container h3 { 634 | display: flex; 635 | align-items: center; 636 | font-size: 1rem; 637 | margin-bottom: var(--space-sm); 638 | } 639 | 640 | .update-packages-container h3::before, 641 | .latest-packages-container h3::before { 642 | content: ""; 643 | display: inline-block; 644 | width: 3px; 645 | height: 14px; 646 | margin-right: var(--space-sm); 647 | border-radius: 2px; 648 | } 649 | 650 | .update-packages-container h3::before { 651 | background-color: var(--warning-color); 652 | } 653 | 654 | .latest-packages-container h3::before { 655 | background-color: var(--success-color); 656 | } 657 | 658 | .packages-list { 659 | max-height: 200px; 660 | overflow-y: auto; 661 | border: 1px solid var(--border-color); 662 | border-radius: var(--radius-md); 663 | background-color: var(--card-color); 664 | scrollbar-width: thin; 665 | scrollbar-color: var(--neutral-400) transparent; 666 | } 667 | 668 | .packages-list::-webkit-scrollbar { 669 | width: 6px; 670 | } 671 | 672 | .packages-list::-webkit-scrollbar-track { 673 | background: transparent; 674 | } 675 | 676 | .packages-list::-webkit-scrollbar-thumb { 677 | background-color: var(--neutral-400); 678 | border-radius: 3px; 679 | } 680 | 681 | .package-item { 682 | display: flex; 683 | padding: var(--space-md); 684 | border-bottom: 1px solid var(--border-color); 685 | align-items: center; 686 | transition: background-color 0.2s; 687 | } 688 | 689 | .package-item:last-child { 690 | border-bottom: none; 691 | } 692 | 693 | .package-item:hover { 694 | background-color: var(--neutral-50); 695 | } 696 | 697 | .package-name { 698 | flex: 1; 699 | font-weight: 500; 700 | overflow: hidden; 701 | text-overflow: ellipsis; 702 | white-space: nowrap; 703 | } 704 | 705 | .package-version, 706 | .package-latest-version { 707 | padding: 3px 8px; 708 | border-radius: var(--radius-sm); 709 | font-size: 0.75rem; 710 | font-family: "SFMono-Regular", Consolas, monospace; 711 | margin-left: var(--space-sm); 712 | background-color: var(--neutral-100); 713 | color: var(--text-color); 714 | } 715 | 716 | .package-latest-version { 717 | background-color: var(--success-color); 718 | color: white; 719 | } 720 | 721 | .no-packages { 722 | padding: var(--space-lg); 723 | text-align: center; 724 | color: var(--secondary-text); 725 | font-style: italic; 726 | } 727 | 728 | .update-confirm-actions { 729 | display: flex; 730 | justify-content: flex-end; 731 | margin-top: var(--space-lg); 732 | gap: var(--space-md); 733 | border-top: 1px solid var(--border-color); 734 | padding-top: var(--space-md); 735 | } 736 | 737 | #update-confirm-proceed { 738 | background-color: var(--warning-color); 739 | } 740 | 741 | #update-confirm-proceed:hover { 742 | background-color: #e67e22; 743 | } 744 | 745 | /* ====================================================== 746 | 版本切换模态框 747 | ====================================================== */ 748 | .version-modal-content { 749 | width: 550px; 750 | max-width: 95%; 751 | } 752 | 753 | /* 标签页导航 */ 754 | .version-tabs { 755 | display: flex; 756 | border-bottom: 1px solid var(--border-color); 757 | margin: var(--space-lg) 0; 758 | } 759 | 760 | .version-tab-btn { 761 | background: transparent; 762 | color: var(--text-color); 763 | border: none; 764 | padding: var(--space-sm) var(--space-md); 765 | margin-right: var(--space-md); 766 | position: relative; 767 | cursor: pointer; 768 | font-weight: 500; 769 | transition: all 0.2s; 770 | } 771 | 772 | .version-tab-btn:hover { 773 | color: var(--primary-color); 774 | background: transparent; 775 | transform: none; 776 | box-shadow: none; 777 | } 778 | 779 | .version-tab-btn.active { 780 | color: var(--primary-color); 781 | } 782 | 783 | .version-tab-btn.active::after { 784 | content: ""; 785 | position: absolute; 786 | bottom: -1px; 787 | left: 0; 788 | width: 100%; 789 | height: 2px; 790 | background-color: var(--primary-color); 791 | border-radius: 2px 2px 0 0; 792 | } 793 | 794 | /* 标签内容 */ 795 | .version-tab-content { 796 | display: none; 797 | margin-top: var(--space-md); 798 | } 799 | 800 | /* 版本列表头部 */ 801 | .version-list-header { 802 | display: flex; 803 | padding: var(--space-sm) var(--space-md); 804 | background-color: var(--neutral-100); 805 | border-radius: var(--radius-sm) var(--radius-sm) 0 0; 806 | border: 1px solid var(--border-color); 807 | font-weight: 600; 808 | font-size: 0.875rem; 809 | color: var(--secondary-text); 810 | } 811 | 812 | /* 版本列表样式 */ 813 | .version-list { 814 | max-height: 300px; 815 | overflow-y: auto; 816 | border: 1px solid var(--border-color); 817 | border-top: none; 818 | border-radius: 0 0 var(--radius-sm) var(--radius-sm); 819 | } 820 | 821 | .version-item { 822 | display: flex; 823 | padding: var(--space-sm) var(--space-md); 824 | border-bottom: 1px solid var(--border-color); 825 | align-items: center; 826 | transition: background-color 0.2s; 827 | } 828 | 829 | .version-item:last-child { 830 | border-bottom: none; 831 | } 832 | 833 | .version-item:hover { 834 | background-color: var(--neutral-50); 835 | } 836 | 837 | .version-name { 838 | flex: 1; 839 | font-weight: 500; 840 | display: flex; 841 | align-items: center; 842 | gap: var(--space-sm); 843 | } 844 | 845 | .version-date { 846 | width: 100px; 847 | color: var(--secondary-text); 848 | font-size: 0.875rem; 849 | } 850 | 851 | .version-action { 852 | width: 120px; 853 | text-align: right; 854 | } 855 | 856 | /* 版本徽章 */ 857 | .version-badge { 858 | display: inline-block; 859 | padding: 2px 6px; 860 | border-radius: var(--radius-sm); 861 | font-size: 0.7rem; 862 | font-weight: 500; 863 | } 864 | 865 | .version-badge.current { 866 | background-color: var(--primary-color); 867 | color: white; 868 | } 869 | 870 | .version-badge.latest { 871 | background-color: var(--success-color); 872 | color: white; 873 | } 874 | 875 | /* 特殊版本项样式 */ 876 | .version-item.current { 877 | background-color: rgba(33, 150, 243, 0.1); 878 | } 879 | 880 | .version-item.latest { 881 | background-color: rgba(0, 184, 148, 0.08); 882 | } 883 | 884 | /* 版本使用按钮 */ 885 | .version-use-btn { 886 | font-size: 0.8rem; 887 | padding: 4px 8px; 888 | background-color: var(--primary-color); 889 | color: white; 890 | border: none; 891 | border-radius: var(--radius-sm); 892 | cursor: pointer; 893 | transition: all 0.2s; 894 | } 895 | 896 | .version-use-btn:disabled { 897 | background-color: var(--neutral-400); 898 | cursor: not-allowed; 899 | opacity: 0.7; 900 | } 901 | 902 | .version-use-btn:not(:disabled):hover { 903 | background-color: var(--primary-dark); 904 | transform: translateY(-1px); 905 | } 906 | 907 | /* 手动切换版本输入 */ 908 | .version-input { 909 | display: flex; 910 | gap: var(--space-md); 911 | margin-top: var(--space-lg); 912 | } 913 | 914 | .version-input input { 915 | flex: 1; 916 | padding: var(--space-sm) var(--space-md); 917 | border: 1px solid var(--border-color); 918 | border-radius: var(--radius-sm); 919 | } 920 | 921 | .version-input input:focus { 922 | outline: none; 923 | border-color: var(--primary-color); 924 | box-shadow: 0 0 0 2px var(--primary-100); 925 | } 926 | 927 | /* 加载和错误状态 */ 928 | .version-loading, .version-error { 929 | padding: var(--space-lg); 930 | text-align: center; 931 | color: var(--secondary-text); 932 | } 933 | 934 | .version-error { 935 | color: var(--error-color); 936 | } 937 | 938 | /* ====================================================== 939 | 环境管理组件 940 | ====================================================== */ 941 | 942 | /* 环境选择按钮 */ 943 | .env-selector { 944 | position: relative; 945 | background-color: var(--primary-color); 946 | color: white; 947 | border: none; 948 | border-radius: var(--radius-md); 949 | padding: 6px 16px; 950 | font-weight: 600; 951 | display: flex; 952 | align-items: center; 953 | justify-content: center; 954 | gap: 8px; 955 | cursor: pointer; 956 | transition: all 0.3s ease; 957 | height: 36px; 958 | min-width: fit-content; 959 | text-align: center; 960 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 961 | } 962 | 963 | .env-selector:hover { 964 | background-color: var(--primary-dark); 965 | transform: translateY(-1px); 966 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 967 | } 968 | 969 | .env-selector:active { 970 | transform: translateY(0); 971 | box-shadow: 0 2px 3px rgba(0, 0, 0, 0.1); 972 | } 973 | 974 | .env-selector .env-icon { 975 | width: 18px; 976 | height: 18px; 977 | fill: currentColor; 978 | transition: transform 0.3s ease; 979 | } 980 | 981 | .env-selector:hover .env-icon { 982 | transform: rotate(180deg); 983 | } 984 | 985 | /* 环境模态框 */ 986 | .environments-modal-content { 987 | width: 90%; 988 | max-width: 800px; 989 | max-height: 85vh; 990 | overflow-y: auto; 991 | } 992 | 993 | .environment-list { 994 | margin: 20px 0; 995 | border: 1px solid var(--border-color); 996 | border-radius: 4px; 997 | overflow: hidden; 998 | } 999 | 1000 | .environment-item { 1001 | padding: 12px; 1002 | display: flex; 1003 | align-items: center; 1004 | border-bottom: 1px solid var(--border-color); 1005 | transition: background-color 0.2s; 1006 | } 1007 | 1008 | .environment-item:last-child { 1009 | border-bottom: none; 1010 | } 1011 | 1012 | .environment-item:hover { 1013 | background-color: var(--hover-bg-color); 1014 | } 1015 | 1016 | .environment-item.active { 1017 | background-color: var(--active-bg-color); 1018 | } 1019 | 1020 | .environment-info { 1021 | flex: 1; 1022 | } 1023 | 1024 | .environment-name { 1025 | font-weight: bold; 1026 | margin-bottom: 4px; 1027 | } 1028 | 1029 | .environment-details { 1030 | font-size: 0.9em; 1031 | color: var(--text-secondary); 1032 | display: flex; 1033 | flex-wrap: wrap; 1034 | gap: 8px; 1035 | } 1036 | 1037 | .environment-detail { 1038 | display: flex; 1039 | align-items: center; 1040 | gap: 4px; 1041 | } 1042 | 1043 | .environment-actions { 1044 | display: flex; 1045 | gap: 8px; 1046 | } 1047 | 1048 | .add-environment-section { 1049 | margin: 20px 0; 1050 | padding: 15px; 1051 | border: 1px solid var(--border-color); 1052 | border-radius: 4px; 1053 | background-color: var(--card-bg-color); 1054 | } 1055 | 1056 | .add-environment-form { 1057 | display: grid; 1058 | grid-template-columns: 1fr 1fr; 1059 | gap: 15px; 1060 | } 1061 | 1062 | .form-field { 1063 | display: flex; 1064 | flex-direction: column; 1065 | gap: 6px; 1066 | } 1067 | 1068 | .form-field label { 1069 | font-weight: bold; 1070 | font-size: 0.9em; 1071 | } 1072 | 1073 | .form-field input { 1074 | padding: 8px; 1075 | border: 1px solid var(--border-color); 1076 | border-radius: 4px; 1077 | background-color: var(--input-bg-color); 1078 | color: var(--text-primary); 1079 | } 1080 | 1081 | .form-actions { 1082 | grid-column: span 2; 1083 | display: flex; 1084 | justify-content: flex-end; 1085 | gap: 10px; 1086 | margin-top: 10px; 1087 | } 1088 | 1089 | .path-input-group { 1090 | display: flex; 1091 | gap: 8px; 1092 | } 1093 | 1094 | .path-input-group input { 1095 | flex: 1; 1096 | } 1097 | 1098 | .path-input-group button { 1099 | white-space: nowrap; 1100 | } 1101 | 1102 | .browse-btn { 1103 | background-color: var(--secondary-button-bg); 1104 | color: var(--secondary-button-text); 1105 | border: 1px solid var(--secondary-button-border); 1106 | border-radius: 4px; 1107 | padding: 0 10px; 1108 | cursor: pointer; 1109 | transition: background-color 0.2s; 1110 | } 1111 | 1112 | .browse-btn:hover { 1113 | background-color: var(--secondary-button-hover-bg); 1114 | } 1115 | 1116 | .no-environments { 1117 | padding: 20px; 1118 | text-align: center; 1119 | color: var(--text-secondary); 1120 | font-style: italic; 1121 | } 1122 | 1123 | .badge { 1124 | display: inline-block; 1125 | padding: 2px 6px; 1126 | border-radius: 10px; 1127 | font-size: 0.7em; 1128 | font-weight: bold; 1129 | text-transform: uppercase; 1130 | color: white; 1131 | } 1132 | 1133 | .badge.current { 1134 | background-color: var(--primary-color); 1135 | } 1136 | 1137 | /* 环境搜索结果 */ 1138 | .search-results { 1139 | margin-top: 15px; 1140 | max-height: 300px; 1141 | overflow-y: auto; 1142 | border: 1px solid var(--border-color); 1143 | border-radius: 4px; 1144 | } 1145 | 1146 | .search-results-header { 1147 | padding: 10px 15px; 1148 | background-color: var(--header-bg-color); 1149 | border-bottom: 1px solid var(--border-color); 1150 | font-weight: bold; 1151 | } 1152 | 1153 | .search-result-item { 1154 | padding: 10px 15px; 1155 | border-bottom: 1px solid var(--border-color); 1156 | display: flex; 1157 | justify-content: space-between; 1158 | align-items: center; 1159 | cursor: pointer; 1160 | transition: background-color 0.2s; 1161 | } 1162 | 1163 | .search-result-item:last-child { 1164 | border-bottom: none; 1165 | } 1166 | 1167 | .search-result-item:hover { 1168 | background-color: var(--hover-bg-color); 1169 | } 1170 | 1171 | .search-result-info { 1172 | flex: 1; 1173 | } 1174 | 1175 | .search-result-name { 1176 | font-weight: bold; 1177 | } 1178 | 1179 | .search-result-path { 1180 | font-size: 0.8em; 1181 | color: var(--text-secondary); 1182 | margin-top: 2px; 1183 | } 1184 | 1185 | .refresh-icon { 1186 | cursor: pointer; 1187 | margin-left: 10px; 1188 | display: inline-flex; 1189 | align-items: center; 1190 | justify-content: center; 1191 | color: var(--text-secondary); 1192 | transition: transform 0.3s; 1193 | } 1194 | 1195 | .refresh-icon:hover { 1196 | color: var(--primary-color); 1197 | } 1198 | 1199 | .refresh-icon.spinning { 1200 | animation: spin 1s linear infinite; 1201 | } 1202 | 1203 | @keyframes spin { 1204 | 0% { transform: rotate(0deg); } 1205 | 100% { transform: rotate(360deg); } 1206 | } 1207 | 1208 | /* ====================================================== 1209 | 依赖关系图 1210 | ====================================================== */ 1211 | .dependency-graph-modal-content { 1212 | width: 800px; 1213 | max-width: 95%; 1214 | max-height: 80vh; 1215 | } 1216 | 1217 | .graph-container { 1218 | height: 500px; 1219 | width: 100%; 1220 | border: 1px solid var(--border-color); 1221 | border-radius: var(--radius-md); 1222 | margin: var(--space-md) 0; 1223 | position: relative; 1224 | overflow: hidden; 1225 | background-color: var(--card-color); 1226 | } 1227 | 1228 | .dependency-graph { 1229 | width: 100%; 1230 | height: 100%; 1231 | } 1232 | 1233 | .graph-loading, .graph-error { 1234 | position: absolute; 1235 | top: 50%; 1236 | left: 50%; 1237 | transform: translate(-50%, -50%); 1238 | text-align: center; 1239 | color: var(--secondary-text); 1240 | font-size: 1rem; 1241 | } 1242 | 1243 | .graph-error { 1244 | color: var(--error-color); 1245 | display: none; 1246 | } 1247 | 1248 | .graph-legend { 1249 | display: flex; 1250 | justify-content: center; 1251 | gap: var(--space-md); 1252 | flex-wrap: wrap; 1253 | margin-top: var(--space-md); 1254 | padding: var(--space-sm) var(--space-lg); 1255 | background-color: var(--neutral-50); 1256 | border-radius: var(--radius-md); 1257 | } 1258 | 1259 | .legend-item { 1260 | display: flex; 1261 | align-items: center; 1262 | gap: var(--space-xs); 1263 | } 1264 | 1265 | .legend-color { 1266 | width: 16px; 1267 | height: 16px; 1268 | border-radius: 50%; 1269 | display: inline-block; 1270 | border: 1px solid var(--border-color); 1271 | } 1272 | 1273 | .legend-color.main-package { 1274 | background-color: #e74c3c; 1275 | } 1276 | 1277 | .legend-color.direct-dependency { 1278 | background-color: #3498db; 1279 | } 1280 | 1281 | .legend-color.optional-dependency { 1282 | background-color: #95a5a6; 1283 | } 1284 | 1285 | .legend-label { 1286 | font-size: 0.875rem; 1287 | color: var(--text-color); 1288 | } 1289 | 1290 | /* D3.js样式 */ 1291 | .node circle { 1292 | stroke-width: 2px; 1293 | } 1294 | 1295 | .node text { 1296 | font-size: 12px; 1297 | font-family: "Segoe UI", sans-serif; 1298 | } 1299 | 1300 | .link { 1301 | fill: none; 1302 | stroke: #999; 1303 | stroke-width: 1.5px; 1304 | stroke-opacity: 0.6; 1305 | } 1306 | 1307 | .link.optional { 1308 | stroke-dasharray: 4; 1309 | } 1310 | 1311 | /* 节点悬停提示框 */ 1312 | .node-tooltip { 1313 | position: fixed; 1314 | padding: var(--space-sm); 1315 | background-color: var(--card-color); 1316 | border: 1px solid var(--border-color); 1317 | border-radius: var(--radius-sm); 1318 | box-shadow: var(--shadow-md); 1319 | font-size: 0.875rem; 1320 | z-index: 1500; 1321 | max-width: 250px; 1322 | pointer-events: none; 1323 | word-wrap: break-word; 1324 | overflow: hidden; 1325 | } 1326 | 1327 | .node-tooltip h4 { 1328 | margin: 0 0 5px 0; 1329 | font-size: 0.95rem; 1330 | color: var(--primary-color); 1331 | } 1332 | 1333 | .node-tooltip p { 1334 | margin: 0; 1335 | color: var(--text-color); 1336 | max-height: 100px; 1337 | overflow-y: auto; 1338 | } 1339 | 1340 | .node-tooltip .version { 1341 | color: var(--secondary-text); 1342 | font-family: monospace; 1343 | font-size: 0.8rem; 1344 | } 1345 | 1346 | /* ====================================================== 1347 | 响应式布局 1348 | ====================================================== */ 1349 | @media (max-width: 768px) { 1350 | /* 优化小屏幕布局 */ 1351 | 1352 | .select-all-container { 1353 | margin-bottom: var(--space-sm); 1354 | margin-right: 0; 1355 | width: 100%; 1356 | justify-content: center; 1357 | } 1358 | } 1359 | -------------------------------------------------------------------------------- /css/modules/utilities.css: -------------------------------------------------------------------------------- 1 | /* Utilities.css - 工具与动画模块 */ 2 | 3 | /* ====================================================== 4 | 动画和过渡效果 5 | ====================================================== */ 6 | /* 基础动画关键帧 */ 7 | @keyframes fadeIn { 8 | from { opacity: 0; } 9 | to { opacity: 1; } 10 | } 11 | 12 | @keyframes fadeOut { 13 | from { opacity: 1; } 14 | to { opacity: 0; } 15 | } 16 | 17 | @keyframes slideDown { 18 | from { transform: translateY(-20px); opacity: 0; } 19 | to { transform: translateY(0); opacity: 1; } 20 | } 21 | 22 | @keyframes slideUp { 23 | from { transform: translateY(0); opacity: 1; } 24 | to { transform: translateY(-20px); opacity: 0; } 25 | } 26 | 27 | @keyframes pulse { 28 | 0% { transform: scale(1); } 29 | 50% { transform: scale(1.05); } 30 | 100% { transform: scale(1); } 31 | } 32 | 33 | @keyframes spin { 34 | from { transform: rotate(0deg); } 35 | to { transform: rotate(360deg); } 36 | } 37 | 38 | @keyframes bounce { 39 | 0%, 100% { transform: translateY(0); } 40 | 50% { transform: translateY(-10px); } 41 | } 42 | 43 | /* 应用动画的工具类 */ 44 | .animate-fade-in { 45 | animation: fadeIn 0.3s ease forwards; 46 | } 47 | 48 | .animate-fade-out { 49 | animation: fadeOut 0.3s ease forwards; 50 | } 51 | 52 | .animate-slide-down { 53 | animation: slideDown 0.4s ease forwards; 54 | } 55 | 56 | .animate-slide-up { 57 | animation: slideUp 0.4s ease forwards; 58 | } 59 | 60 | .animate-pulse { 61 | animation: pulse 1.5s infinite; 62 | } 63 | 64 | .animate-spin { 65 | animation: spin 1s linear infinite; 66 | } 67 | 68 | .animate-bounce { 69 | animation: bounce 1s infinite; 70 | } 71 | 72 | /* 过渡效果 */ 73 | .transition-all { 74 | transition: all 0.3s ease; 75 | } 76 | 77 | .transition-transform { 78 | transition: transform 0.3s ease; 79 | } 80 | 81 | .transition-opacity { 82 | transition: opacity 0.3s ease; 83 | } 84 | 85 | .transition-colors { 86 | transition: background-color 0.3s ease, color 0.3s ease; 87 | } 88 | 89 | /* ====================================================== 90 | 布局辅助类 91 | ====================================================== */ 92 | /* Flexbox 工具类 */ 93 | .flex { 94 | display: flex; 95 | } 96 | 97 | .flex-col { 98 | display: flex; 99 | flex-direction: column; 100 | } 101 | 102 | .flex-wrap { 103 | flex-wrap: wrap; 104 | } 105 | 106 | .flex-nowrap { 107 | flex-wrap: nowrap; 108 | } 109 | 110 | .items-center { 111 | align-items: center; 112 | } 113 | 114 | .items-start { 115 | align-items: flex-start; 116 | } 117 | 118 | .items-end { 119 | align-items: flex-end; 120 | } 121 | 122 | .justify-center { 123 | justify-content: center; 124 | } 125 | 126 | .justify-between { 127 | justify-content: space-between; 128 | } 129 | 130 | .justify-start { 131 | justify-content: flex-start; 132 | } 133 | 134 | .justify-end { 135 | justify-content: flex-end; 136 | } 137 | 138 | .flex-1 { 139 | flex: 1; 140 | } 141 | 142 | .flex-auto { 143 | flex: 1 1 auto; 144 | } 145 | 146 | .flex-none { 147 | flex: none; 148 | } 149 | 150 | /* Grid 工具类 */ 151 | .grid { 152 | display: grid; 153 | } 154 | 155 | .grid-cols-2 { 156 | grid-template-columns: repeat(2, 1fr); 157 | } 158 | 159 | .grid-cols-3 { 160 | grid-template-columns: repeat(3, 1fr); 161 | } 162 | 163 | .grid-cols-4 { 164 | grid-template-columns: repeat(4, 1fr); 165 | } 166 | 167 | .gap-xs { 168 | gap: var(--space-xs); 169 | } 170 | 171 | .gap-sm { 172 | gap: var(--space-sm); 173 | } 174 | 175 | .gap-md { 176 | gap: var(--space-md); 177 | } 178 | 179 | .gap-lg { 180 | gap: var(--space-lg); 181 | } 182 | 183 | /* ====================================================== 184 | 间距和尺寸工具类 185 | ====================================================== */ 186 | /* Margin 工具类 */ 187 | .m-0 { margin: 0; } 188 | .mx-auto { margin-left: auto; margin-right: auto; } 189 | .mt-xs { margin-top: var(--space-xs); } 190 | .mt-sm { margin-top: var(--space-sm); } 191 | .mt-md { margin-top: var(--space-md); } 192 | .mt-lg { margin-top: var(--space-lg); } 193 | 194 | .mb-xs { margin-bottom: var(--space-xs); } 195 | .mb-sm { margin-bottom: var(--space-sm); } 196 | .mb-md { margin-bottom: var(--space-md); } 197 | .mb-lg { margin-bottom: var(--space-lg); } 198 | 199 | .ml-xs { margin-left: var(--space-xs); } 200 | .ml-sm { margin-left: var(--space-sm); } 201 | .ml-md { margin-left: var(--space-md); } 202 | .ml-lg { margin-left: var(--space-lg); } 203 | 204 | .mr-xs { margin-right: var(--space-xs); } 205 | .mr-sm { margin-right: var(--space-sm); } 206 | .mr-md { margin-right: var(--space-md); } 207 | .mr-lg { margin-right: var(--space-lg); } 208 | 209 | /* Padding 工具类 */ 210 | .p-0 { padding: 0; } 211 | .p-xs { padding: var(--space-xs); } 212 | .p-sm { padding: var(--space-sm); } 213 | .p-md { padding: var(--space-md); } 214 | .p-lg { padding: var(--space-lg); } 215 | 216 | .px-xs { padding-left: var(--space-xs); padding-right: var(--space-xs); } 217 | .px-sm { padding-left: var(--space-sm); padding-right: var(--space-sm); } 218 | .px-md { padding-left: var(--space-md); padding-right: var(--space-md); } 219 | .px-lg { padding-left: var(--space-lg); padding-right: var(--space-lg); } 220 | 221 | .py-xs { padding-top: var(--space-xs); padding-bottom: var(--space-xs); } 222 | .py-sm { padding-top: var(--space-sm); padding-bottom: var(--space-sm); } 223 | .py-md { padding-top: var(--space-md); padding-bottom: var(--space-md); } 224 | .py-lg { padding-top: var(--space-lg); padding-bottom: var(--space-lg); } 225 | 226 | /* 尺寸工具类 */ 227 | .w-full { width: 100%; } 228 | .w-auto { width: auto; } 229 | .h-full { height: 100%; } 230 | .h-auto { height: auto; } 231 | .max-w-xs { max-width: 320px; } 232 | .max-w-sm { max-width: 480px; } 233 | .max-w-md { max-width: 640px; } 234 | .max-w-lg { max-width: 800px; } 235 | .max-w-xl { max-width: 1024px; } 236 | 237 | /* ====================================================== 238 | 文本样式工具类 239 | ====================================================== */ 240 | .text-xs { font-size: 0.75rem; } 241 | .text-sm { font-size: 0.875rem; } 242 | .text-md { font-size: 1rem; } 243 | .text-lg { font-size: 1.125rem; } 244 | .text-xl { font-size: 1.25rem; } 245 | .text-2xl { font-size: 1.5rem; } 246 | .text-3xl { font-size: 1.875rem; } 247 | .text-4xl { font-size: 2.25rem; } 248 | 249 | .font-normal { font-weight: 400; } 250 | .font-medium { font-weight: 500; } 251 | .font-semibold { font-weight: 600; } 252 | .font-bold { font-weight: 700; } 253 | 254 | .text-left { text-align: left; } 255 | .text-center { text-align: center; } 256 | .text-right { text-align: right; } 257 | 258 | .text-primary { color: var(--primary-color); } 259 | .text-secondary { color: var(--secondary-text); } 260 | .text-success { color: var(--success-color); } 261 | .text-warning { color: var(--warning-color); } 262 | .text-error { color: var(--error-color); } 263 | .text-info { color: var(--info-color); } 264 | 265 | .line-clamp-1 { 266 | display: -webkit-box; 267 | -webkit-line-clamp: 1; 268 | -webkit-box-orient: vertical; 269 | overflow: hidden; 270 | } 271 | 272 | .line-clamp-2 { 273 | display: -webkit-box; 274 | -webkit-line-clamp: 2; 275 | -webkit-box-orient: vertical; 276 | overflow: hidden; 277 | } 278 | 279 | .line-clamp-3 { 280 | display: -webkit-box; 281 | -webkit-line-clamp: 3; 282 | -webkit-box-orient: vertical; 283 | overflow: hidden; 284 | } 285 | 286 | .truncate { 287 | overflow: hidden; 288 | text-overflow: ellipsis; 289 | white-space: nowrap; 290 | } 291 | 292 | /* ====================================================== 293 | 样式辅助类 294 | ====================================================== */ 295 | /* Display工具类 */ 296 | .block { display: block; } 297 | .inline-block { display: inline-block; } 298 | .inline { display: inline; } 299 | .hidden { display: none; } 300 | .invisible { visibility: hidden; } 301 | 302 | /* Position工具类 */ 303 | .relative { position: relative; } 304 | .absolute { position: absolute; } 305 | .fixed { position: fixed; } 306 | .sticky { position: sticky; } 307 | 308 | .top-0 { top: 0; } 309 | .bottom-0 { bottom: 0; } 310 | .left-0 { left: 0; } 311 | .right-0 { right: 0; } 312 | 313 | /* 边框和圆角工具类 */ 314 | .rounded-sm { border-radius: var(--radius-sm); } 315 | .rounded-md { border-radius: var(--radius-md); } 316 | .rounded-lg { border-radius: var(--radius-lg); } 317 | .rounded-full { border-radius: 9999px; } 318 | 319 | .border { border: 1px solid var(--border-color); } 320 | .border-t { border-top: 1px solid var(--border-color); } 321 | .border-b { border-bottom: 1px solid var(--border-color); } 322 | .border-l { border-left: 1px solid var(--border-color); } 323 | .border-r { border-right: 1px solid var(--border-color); } 324 | 325 | /* 阴影工具类 */ 326 | .shadow-none { box-shadow: none; } 327 | .shadow-sm { box-shadow: var(--shadow-sm); } 328 | .shadow-md { box-shadow: var(--shadow-md); } 329 | .shadow-lg { box-shadow: var(--shadow-lg); } 330 | 331 | /* 交互状态工具类 */ 332 | .pointer { cursor: pointer; } 333 | .select-none { user-select: none; } 334 | .select-text { user-select: text; } 335 | 336 | /* 透明度工具类 */ 337 | .opacity-0 { opacity: 0; } 338 | .opacity-25 { opacity: 0.25; } 339 | .opacity-50 { opacity: 0.5; } 340 | .opacity-75 { opacity: 0.75; } 341 | .opacity-100 { opacity: 1; } 342 | 343 | /* ====================================================== 344 | 背景颜色工具类 345 | ====================================================== */ 346 | .bg-transparent { background-color: transparent; } 347 | .bg-primary { background-color: var(--primary-color); } 348 | .bg-primary-light { background-color: var(--primary-light); } 349 | .bg-primary-dark { background-color: var(--primary-dark); } 350 | .bg-card { background-color: var(--card-color); } 351 | .bg-success { background-color: var(--success-color); } 352 | .bg-warning { background-color: var(--warning-color); } 353 | .bg-error { background-color: var(--error-color); } 354 | .bg-info { background-color: var(--info-color); } 355 | 356 | /* ====================================================== 357 | 响应式设计辅助工具 358 | ====================================================== */ 359 | /* 显示/隐藏工具类 */ 360 | @media (max-width: 640px) { 361 | .hidden-xs { display: none; } 362 | .block-xs { display: block; } 363 | .flex-xs { display: flex; } 364 | } 365 | 366 | @media (min-width: 641px) and (max-width: 768px) { 367 | .hidden-sm { display: none; } 368 | .block-sm { display: block; } 369 | .flex-sm { display: flex; } 370 | } 371 | 372 | @media (min-width: 769px) and (max-width: 1024px) { 373 | .hidden-md { display: none; } 374 | .block-md { display: block; } 375 | .flex-md { display: flex; } 376 | } 377 | 378 | @media (min-width: 1025px) { 379 | .hidden-lg { display: none; } 380 | .block-lg { display: block; } 381 | .flex-lg { display: flex; } 382 | } 383 | 384 | /* 响应式栅格 */ 385 | @media (max-width: 640px) { 386 | .grid-cols-xs-1 { grid-template-columns: 1fr; } 387 | .grid-cols-xs-2 { grid-template-columns: repeat(2, 1fr); } 388 | } 389 | 390 | @media (min-width: 641px) and (max-width: 768px) { 391 | .grid-cols-sm-2 { grid-template-columns: repeat(2, 1fr); } 392 | .grid-cols-sm-3 { grid-template-columns: repeat(3, 1fr); } 393 | } 394 | 395 | @media (min-width: 769px) and (max-width: 1024px) { 396 | .grid-cols-md-3 { grid-template-columns: repeat(3, 1fr); } 397 | .grid-cols-md-4 { grid-template-columns: repeat(4, 1fr); } 398 | } 399 | 400 | /* 打印样式辅助 */ 401 | @media print { 402 | .print-hidden { display: none !important; } 403 | .print-break-before { page-break-before: always; } 404 | .print-break-after { page-break-after: always; } 405 | .print-no-break { page-break-inside: avoid; } 406 | } 407 | 408 | /* ====================================================== 409 | 辅助内容样式 410 | ====================================================== */ 411 | .sr-only { 412 | position: absolute; 413 | width: 1px; 414 | height: 1px; 415 | padding: 0; 416 | margin: -1px; 417 | overflow: hidden; 418 | clip: rect(0, 0, 0, 0); 419 | white-space: nowrap; 420 | border-width: 0; 421 | } 422 | 423 | .focus-outline { 424 | outline: 2px solid var(--primary-color); 425 | outline-offset: 2px; 426 | } 427 | 428 | .clearfix::after { 429 | content: ""; 430 | display: table; 431 | clear: both; 432 | } 433 | 434 | /* 实用ARIA辅助类 */ 435 | [aria-busy="true"] { 436 | cursor: progress; 437 | } 438 | 439 | [aria-disabled="true"] { 440 | cursor: not-allowed; 441 | opacity: 0.7; 442 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AI环境管理工具 5 | 6 | 7 | 8 |
9 |

AI环境管理工具

10 |
11 |
12 | 19 | 加载中... 20 | 加载中... 21 |
22 | 34 |
35 |
36 |
37 |
38 | 39 | 44 |
45 |
46 | 47 |
48 | 49 |
50 |
51 |
52 | 53 | 54 |
55 |
56 | 64 |
65 |
66 |
67 | 73 | 79 | 85 |
86 |
87 | 88 |
89 | 90 | 依赖名称 91 | 版本 92 | 描述 93 | 操作 94 |
95 |
96 |
97 | 98 | 99 | 100 | 加载依赖列表中... 101 |
102 |
103 |
104 | 105 | 106 |
107 | 108 |
109 |
110 |

包安装

111 |
112 |
113 |
114 |
115 | 116 | 117 |
118 | 124 | 130 |
131 |
132 |
133 |
134 |
135 |
136 | 147 | 148 | 155 | 156 | 186 | 187 | 188 | 216 | 217 | 218 | 245 | 246 | 249 |
250 | 251 | 252 | 253 | 254 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | // 主入口文件 - 导入模块并初始化应用 2 | 3 | // 导入必要的模块 4 | import { loadDependencies, checkDescriptionUpdates, checkLatestVersions, refreshDependencies } from './modules/core-api.js'; 5 | import { showNotification, showLoadingMessage, 6 | showInitialLoadingScreen, hideInitialLoadingScreen, 7 | updateInitialLoadingProgress } from './modules/ui-components.js'; 8 | import { dependencyHandler, environmentManager } from './modules/dependency-manager.js'; 9 | 10 | // 记录最后一次描述更新检查时间 11 | let lastDescriptionUpdateCheck = 0; 12 | // 描述更新检查间隔 (毫秒) 13 | const DESCRIPTION_CHECK_INTERVAL = 30000; 14 | 15 | // 添加软件启动状态标志 16 | let appInitialized = false; 17 | 18 | /** 19 | * 启动描述更新检查器 20 | */ 21 | function startDescriptionUpdateChecker() { 22 | // 每30秒检查一次是否有描述更新 23 | setInterval(async () => { 24 | // 只有应用初始化完成后才执行 25 | if (appInitialized) { 26 | const result = await checkDescriptionUpdates(lastDescriptionUpdateCheck / 1000); 27 | if (result.hasUpdates) { 28 | console.log('发现依赖描述更新,正在刷新...'); 29 | await refreshDependencies(true); // 使用缓存刷新,不重复检查版本 30 | } 31 | lastDescriptionUpdateCheck = Date.now(); 32 | } 33 | }, DESCRIPTION_CHECK_INTERVAL); 34 | } 35 | 36 | /** 37 | * 加载系统信息 38 | */ 39 | async function loadSystemInfo() { 40 | try { 41 | updateInitialLoadingProgress(70, '加载系统信息...'); 42 | console.log('正在加载系统信息...'); 43 | const response = await fetch('/api/system-info'); 44 | 45 | if (!response.ok) { 46 | throw new Error(`请求失败: ${response.status}`); 47 | } 48 | 49 | const data = await response.json(); 50 | 51 | // 使用更简洁的方式更新DOM元素 52 | const elements = { 53 | 'python-version': `Python: ${data.pythonVersion}`, 54 | 'pip-version': `Pip: ${data.pipVersion}` 55 | }; 56 | 57 | // 批量更新元素内容 58 | Object.entries(elements).forEach(([id, text]) => { 59 | const element = document.getElementById(id); 60 | if (element) element.textContent = text; 61 | }); 62 | 63 | updateInitialLoadingProgress(80, '系统信息加载完成'); 64 | } catch (error) { 65 | console.error('加载系统信息失败:', error); 66 | // 同样使用简洁方式更新错误状态 67 | ['python-version', 'pip-version'].forEach(id => { 68 | const element = document.getElementById(id); 69 | if (element) element.textContent = id.includes('python') 70 | ? 'Python: 加载失败' : 'Pip: 加载失败'; 71 | }); 72 | updateInitialLoadingProgress(80, '系统信息加载失败,继续初始化...'); 73 | } 74 | } 75 | 76 | /** 77 | * 设置所有事件监听器 78 | */ 79 | function setupEventListeners() { 80 | // 更新进度 81 | updateInitialLoadingProgress(85, '设置事件监听...'); 82 | 83 | // 设置依赖管理相关事件 84 | dependencyHandler.setupDependencyEvents(); 85 | 86 | // 设置环境管理相关事件 87 | environmentManager.setupEnvironmentEvents(); 88 | 89 | updateInitialLoadingProgress(90, '事件初始化完成'); 90 | } 91 | 92 | /** 93 | * 初始化应用所有功能模块 94 | */ 95 | async function initializeAppFeatures() { 96 | updateInitialLoadingProgress(95, '初始化应用功能...'); 97 | 98 | // 记录当前时间作为初始检查点 99 | lastDescriptionUpdateCheck = Date.now(); 100 | 101 | // 先初始化环境管理器,确保环境信息已加载 102 | await environmentManager.initialize(); 103 | 104 | // 启动描述更新检查器(仅设置定时器,不立即执行) 105 | startDescriptionUpdateChecker(); 106 | 107 | console.log('所有应用功能已初始化'); 108 | updateInitialLoadingProgress(100, '初始化完成!'); 109 | // 短暂延迟后隐藏加载界面,让用户看到100%的完成状态 110 | setTimeout(async () => { 111 | hideInitialLoadingScreen(); 112 | appInitialized = true; 113 | 114 | // 应用完全初始化后,触发一次性的依赖描述更新 115 | console.log('应用已完全初始化,开始检查缺失的依赖描述...'); 116 | const result = await checkDescriptionUpdates(lastDescriptionUpdateCheck / 1000, true); // 使用isFirstLoad=true标记首次加载 117 | if (result.hasUpdates) { 118 | console.log('发现缺失的依赖描述,正在刷新...'); 119 | await refreshDependencies(true); // 使用缓存刷新,不重复检查版本 120 | } 121 | lastDescriptionUpdateCheck = Date.now(); 122 | }, 500); 123 | } 124 | 125 | /** 126 | * 模拟加载进度 - 优化版 127 | * 更平滑地模拟进度,支持非线性进度增长 128 | * @param {Function} updateCallback - 进度更新回调 129 | * @param {number} startPercent - 起始百分比 130 | * @param {number} endPercent - 结束百分比 131 | * @param {number} duration - 总持续时间(毫秒) 132 | * @param {string} message - 进度消息 133 | * @param {string} curve - 进度曲线类型 ('linear', 'ease-out') 134 | */ 135 | function simulateProgress(updateCallback, startPercent, endPercent, duration, message, curve = 'linear') { 136 | const stepCount = 20; // 增加步数,使进度更平滑 137 | const stepTime = duration / stepCount; 138 | 139 | updateCallback(startPercent, message); 140 | 141 | let currentStep = 0; 142 | const interval = setInterval(() => { 143 | currentStep++; 144 | if (currentStep <= stepCount) { 145 | let progress; 146 | const ratio = currentStep / stepCount; 147 | 148 | // 根据不同曲线类型计算进度 149 | if (curve === 'ease-out') { 150 | // 缓出效果:开始快,结束慢 151 | progress = startPercent + (endPercent - startPercent) * (1 - Math.pow(1 - ratio, 2)); 152 | } else if (curve === 'ease-in') { 153 | // 缓入效果:开始慢,结束快 154 | progress = startPercent + (endPercent - startPercent) * (ratio * ratio); 155 | } else { 156 | // 线性 157 | progress = startPercent + (endPercent - startPercent) * ratio; 158 | } 159 | 160 | updateCallback(Math.round(progress), message); 161 | } else { 162 | clearInterval(interval); 163 | } 164 | }, stepTime); 165 | 166 | return { 167 | clear: () => clearInterval(interval), 168 | complete: () => { 169 | clearInterval(interval); 170 | updateCallback(endPercent, message); 171 | } 172 | }; 173 | } 174 | 175 | // 应用初始化函数 176 | async function initApp() { 177 | try { 178 | console.log('正在初始化应用...'); 179 | 180 | // 显示初始加载屏幕 181 | showInitialLoadingScreen(async () => { 182 | // 阶段1: 初始连接 (0-20%) 183 | const initialProgress = simulateProgress( 184 | updateInitialLoadingProgress, 185 | 0, 20, 186 | 800, 187 | '正在连接服务...', 188 | 'ease-out' 189 | ); 190 | 191 | // 等待服务连接 192 | setTimeout(async () => { 193 | initialProgress.complete(); 194 | 195 | // 阶段2: 准备加载依赖数据 (20-30%) 196 | updateInitialLoadingProgress(20, '正在准备加载依赖数据...'); 197 | 198 | try { 199 | // 阶段3: 获取依赖列表 (30-50%) 200 | const dependencyListProgress = simulateProgress( 201 | updateInitialLoadingProgress, 202 | 30, 50, 203 | 1000, 204 | '正在获取依赖列表...' 205 | ); 206 | 207 | // 刷新依赖列表时的回调函数,用于更新进度 208 | const progressCallback = (stage, detail) => { 209 | if (stage === 'start') { 210 | dependencyListProgress.complete(); 211 | } else if (stage === 'loading') { 212 | updateInitialLoadingProgress(55, '正在加载依赖信息...'); 213 | } else if (stage === 'processing') { 214 | updateInitialLoadingProgress(70, '正在处理依赖数据...'); 215 | } 216 | }; 217 | 218 | // 刷新依赖列表,传入进度回调 219 | await refreshDependencies(false, progressCallback); 220 | 221 | // 阶段4: 加载系统信息 (70-80%) 222 | updateInitialLoadingProgress(70, '正在加载系统信息...'); 223 | await loadSystemInfo(); 224 | 225 | // 阶段5: 设置事件监听和初始化组件 (80-95%) 226 | const finalSetupProgress = simulateProgress( 227 | updateInitialLoadingProgress, 228 | 80, 95, 229 | 300, 230 | '正在初始化界面组件...' 231 | ); 232 | 233 | // 设置事件监听器 234 | setupEventListeners(); 235 | finalSetupProgress.complete(); 236 | 237 | // 阶段6: 最终初始化 (95-100%) 238 | // 初始化应用功能 239 | initializeAppFeatures(); 240 | 241 | } catch (error) { 242 | console.error('依赖数据加载失败:', error); 243 | updateInitialLoadingProgress(100, '加载失败,请刷新页面重试'); 244 | showNotification('应用初始化失败,请刷新页面重试', 'error'); 245 | hideInitialLoadingScreen(); 246 | } 247 | }, 800); 248 | }); 249 | 250 | } catch (error) { 251 | console.error('应用初始化失败:', error); 252 | showLoadingMessage(`加载失败: ${error.message}`); 253 | hideInitialLoadingScreen(); 254 | } 255 | } 256 | 257 | // 页面加载完成后初始化应用 258 | document.addEventListener('DOMContentLoaded', initApp); 259 | 260 | // 添加全局错误处理 261 | window.addEventListener('error', function(event) { 262 | console.error('全局错误:', event.error); 263 | showNotification(`发生错误: ${event.error.message}`, 'error'); 264 | }); 265 | 266 | // 导出refreshDependencies供其他模块使用 267 | export { refreshDependencies }; 268 | -------------------------------------------------------------------------------- /js/modules/core-api.js: -------------------------------------------------------------------------------- 1 | // 核心API层 - 处理与后端的通信和核心业务逻辑 2 | 3 | const API_BASE_URL = 'http://127.0.0.1:8282/api'; 4 | 5 | // ========== API通信相关函数 ========== 6 | 7 | /** 8 | * 基础API请求函数,包含错误处理 9 | * @param {string} url - API URL 10 | * @param {Object} options - 请求选项 11 | * @returns {Promise} - 响应数据 12 | */ 13 | async function apiRequest(url, options = {}) { 14 | try { 15 | // 添加超时控制 16 | const controller = new AbortController(); 17 | const timeoutId = setTimeout(() => controller.abort(), 30000); // 30秒超时 18 | 19 | // 添加Cache-Control头,确保每次请求都获取最新数据 20 | const headers = { 21 | 'Cache-Control': 'no-cache, no-store, must-revalidate', 22 | 'Pragma': 'no-cache', 23 | 'Expires': '0', 24 | ...(options.headers || {}) 25 | }; 26 | 27 | const response = await fetch(url, { 28 | ...options, 29 | headers, 30 | signal: controller.signal 31 | }); 32 | 33 | clearTimeout(timeoutId); // 清除超时 34 | 35 | if (!response.ok) { 36 | const errorText = await response.text(); 37 | throw new Error(`请求失败 (${response.status}): ${errorText}`); 38 | } 39 | 40 | return await response.json(); 41 | } catch (error) { 42 | if (error.name === 'AbortError') { 43 | throw new Error('请求超时,请检查网络连接'); 44 | } 45 | throw error; 46 | } 47 | } 48 | 49 | /** 50 | * 获取缓存信息 51 | * @returns {Promise} 缓存信息 52 | */ 53 | export async function getCacheInfo() { 54 | return await apiRequest(`${API_BASE_URL}/cache-info`); 55 | } 56 | 57 | /** 58 | * 加载所有依赖列表 59 | * @param {boolean} useCache - 是否使用缓存 60 | * @returns {Promise} 依赖数组 61 | */ 62 | export async function loadDependencies(useCache = false) { 63 | try { 64 | const response = await fetch(`/api/dependencies?useCache=${useCache}`); 65 | if (!response.ok) { 66 | throw new Error(`请求失败: ${response.status}`); 67 | } 68 | return await response.json(); 69 | } catch (error) { 70 | console.error('加载依赖失败:', error); 71 | throw error; 72 | } 73 | } 74 | 75 | /** 76 | * 卸载单个依赖 77 | * @param {string} dependency - 依赖名称 78 | * @returns {Promise} 响应对象 79 | */ 80 | export async function uninstallDependency(dependency) { 81 | try { 82 | const response = await fetch('/api/uninstall', { 83 | method: 'POST', 84 | headers: { 85 | 'Content-Type': 'application/json' 86 | }, 87 | body: JSON.stringify({ dependency }) 88 | }); 89 | 90 | if (!response.ok) { 91 | const data = await response.json(); 92 | return { success: false, message: data.message || `卸载失败: ${response.status}` }; 93 | } 94 | 95 | return await response.json(); 96 | } catch (error) { 97 | console.error('卸载依赖失败:', error); 98 | return { success: false, message: `卸载失败: ${error.message}` }; 99 | } 100 | } 101 | 102 | /** 103 | * 更新依赖到最新版本 104 | * @param {string} dependency - 依赖名称 105 | * @returns {Promise} 响应对象 106 | */ 107 | export async function updateDependency(dependency) { 108 | try { 109 | const response = await fetch('/api/update', { 110 | method: 'POST', 111 | headers: { 112 | 'Content-Type': 'application/json' 113 | }, 114 | body: JSON.stringify({ dependency }) 115 | }); 116 | 117 | return await response.json(); 118 | } catch (error) { 119 | console.error('更新依赖失败:', error); 120 | return { success: false, message: `更新失败: ${error.message}` }; 121 | } 122 | } 123 | 124 | /** 125 | * 获取依赖的当前版本 126 | * @param {string} dependency - 依赖名称 127 | * @returns {Promise} 响应对象,包含当前版本信息 128 | */ 129 | export async function getCurrentVersion(dependency) { 130 | try { 131 | const response = await fetch(`/api/dependency-current-version/${encodeURIComponent(dependency)}`); 132 | 133 | if (!response.ok) { 134 | const data = await response.json(); 135 | return { 136 | success: false, 137 | message: data.message || `获取版本信息失败: ${response.status}` 138 | }; 139 | } 140 | 141 | return await response.json(); 142 | } catch (error) { 143 | console.error('获取版本信息失败:', error); 144 | return { success: false, message: `获取版本信息失败: ${error.message}` }; 145 | } 146 | } 147 | 148 | /** 149 | * 切换依赖版本 150 | * @param {string} dependency - 依赖名称 151 | * @param {string} version - 目标版本 152 | * @returns {Promise} 响应对象 153 | */ 154 | export async function switchVersion(dependency, version) { 155 | try { 156 | const response = await fetch('/api/switch-version', { 157 | method: 'POST', 158 | headers: { 159 | 'Content-Type': 'application/json' 160 | }, 161 | body: JSON.stringify({ dependency, version }) 162 | }); 163 | 164 | return await response.json(); 165 | } catch (error) { 166 | console.error('版本切换失败:', error); 167 | return { success: false, message: `版本切换失败: ${error.message}` }; 168 | } 169 | } 170 | 171 | /** 172 | * 批量卸载依赖 173 | * @param {Array} packages - 包名称数组 174 | * @returns {Promise} 响应对象 175 | */ 176 | export async function batchUninstall(packages) { 177 | try { 178 | // 确保输入是字符串数组 179 | const packageArray = Array.isArray(packages) ? packages : [packages]; 180 | 181 | const response = await fetch('/api/batch-uninstall', { 182 | method: 'POST', 183 | headers: { 184 | 'Content-Type': 'application/json' 185 | }, 186 | body: JSON.stringify({ packages: packageArray }) 187 | }); 188 | 189 | return await response.json(); 190 | } catch (error) { 191 | console.error('批量卸载失败:', error); 192 | return { success: false, message: `批量卸载失败: ${error.message}` }; 193 | } 194 | } 195 | 196 | /** 197 | * 获取任务进度 198 | * @param {string} taskId - 任务ID 199 | * @returns {Promise} 进度对象 200 | */ 201 | export async function getTaskProgress(taskId) { 202 | try { 203 | const response = await fetch(`/api/task-progress/${taskId}`); 204 | 205 | if (!response.ok) { 206 | throw new Error(`获取任务进度失败: ${response.status}`); 207 | } 208 | 209 | return await response.json(); 210 | } catch (error) { 211 | console.error('获取任务进度失败:', error); 212 | return { error: error.message }; 213 | } 214 | } 215 | 216 | /** 217 | * 检查描述更新 218 | * @param {number} lastUpdate - 上次更新时间戳 219 | * @param {boolean} isFirstLoad - 是否为UI首次加载后的请求 220 | * @param {boolean} environmentChanged - 环境是否发生变更 221 | * @returns {Promise} 响应对象 222 | */ 223 | export async function checkDescriptionUpdates(lastUpdate, isFirstLoad = false, environmentChanged = false) { 224 | try { 225 | // 如果是UI首次加载后的请求,传递特殊的lastUpdate值 226 | const timestamp = isFirstLoad ? 0 : lastUpdate; 227 | 228 | const url = new URL('/api/check-description-updates', window.location.origin); 229 | url.searchParams.append('lastUpdate', timestamp); 230 | if (environmentChanged) { 231 | url.searchParams.append('environmentChanged', 'true'); 232 | } 233 | 234 | const response = await fetch(url.toString()); 235 | 236 | if (!response.ok) { 237 | return { hasUpdates: false }; 238 | } 239 | 240 | const result = await response.json(); 241 | 242 | // 添加日志 243 | if (isFirstLoad) { 244 | console.log('UI加载完成,已向服务器请求更新缺失的依赖描述'); 245 | } else if (environmentChanged) { 246 | console.log('Python环境已切换,已向服务器请求更新依赖描述'); 247 | } 248 | 249 | return result; 250 | } catch (error) { 251 | console.error('检查描述更新失败:', error); 252 | return { hasUpdates: false }; 253 | } 254 | } 255 | 256 | /** 257 | * 获取依赖的可用版本历史 258 | * @param {string} dependency - 依赖名称 259 | */ 260 | export async function getPackageVersions(dependency) { 261 | try { 262 | const response = await fetch(`https://pypi.org/pypi/${dependency}/json`); 263 | 264 | if (!response.ok) { 265 | throw new Error(`获取版本历史失败: ${response.status}`); 266 | } 267 | 268 | const data = await response.json(); 269 | const releases = data.releases || {}; 270 | const versions = []; 271 | 272 | // 处理每个版本 273 | Object.keys(releases).forEach(version => { 274 | const releaseFiles = releases[version]; 275 | if (releaseFiles && releaseFiles.length > 0) { 276 | // 获取上传日期 277 | const uploadTime = releaseFiles[0].upload_time; 278 | versions.push({ 279 | version: version, 280 | normalizedVersion: normalizeVersion(version), // 添加标准化版本 281 | uploadDate: uploadTime ? new Date(uploadTime) : null 282 | }); 283 | } 284 | }); 285 | 286 | // 按上传时间排序,最新的在前 287 | versions.sort((a, b) => { 288 | if (a.uploadDate && b.uploadDate) { 289 | return b.uploadDate - a.uploadDate; 290 | } 291 | return 0; 292 | }); 293 | 294 | const latestVersion = data.info.version || null; 295 | 296 | return { 297 | success: true, 298 | versions: versions, 299 | latestVersion: latestVersion, 300 | normalizedLatestVersion: normalizeVersion(latestVersion) 301 | }; 302 | } catch (error) { 303 | console.error('获取版本历史出错:', error); 304 | return { 305 | success: false, 306 | message: error.message, 307 | versions: [] 308 | }; 309 | } 310 | } 311 | 312 | /** 313 | * 标准化版本号用于比较 314 | * @param {string} version - 版本号 315 | * @returns {string} 标准化后的版本号 316 | */ 317 | function normalizeVersion(version) { 318 | if (!version) return ''; 319 | 320 | let normalized = String(version); 321 | 322 | // 移除.postX后缀 323 | if (normalized.includes('.post')) { 324 | normalized = normalized.split('.post')[0]; 325 | } 326 | 327 | // 移除预发布标识 328 | const prefixes = ['a', 'b', 'rc', 'dev', 'alpha', 'beta', 'pre']; 329 | for (const prefix of prefixes) { 330 | if (normalized.includes(`.${prefix}`)) { 331 | normalized = normalized.split(`.${prefix}`)[0]; 332 | } 333 | if (normalized.includes(`-${prefix}`)) { 334 | normalized = normalized.split(`-${prefix}`)[0]; 335 | } 336 | } 337 | 338 | return normalized; 339 | } 340 | 341 | /** 342 | * 更新所选依赖 343 | * @param {Array} packageNames - 包名称数组 344 | * @returns {Promise} 345 | */ 346 | export async function updateSelected(packageNames) { 347 | try { 348 | // 确保我们只发送包名数组,而不是包含其他信息的复杂对象 349 | const packagesToUpdate = Array.isArray(packageNames) ? 350 | packageNames.map(pkg => typeof pkg === 'string' ? pkg : pkg.name) : 351 | []; 352 | 353 | const response = await fetch('/api/update-selected', { 354 | method: 'POST', 355 | headers: { 356 | 'Content-Type': 'application/json' 357 | }, 358 | body: JSON.stringify({ packages: packagesToUpdate }) 359 | }); 360 | 361 | return await response.json(); 362 | } catch (error) { 363 | console.error('更新所选依赖失败:', error); 364 | return { success: false, message: `更新失败: ${error.message}` }; 365 | } 366 | } 367 | 368 | /** 369 | * 安装依赖 370 | * @param {string} packageName - 包名称 371 | * @returns {Promise} 响应对象 372 | */ 373 | export async function installDependency(packageName) { 374 | try { 375 | // 添加正确的Content-Type头 376 | const response = await fetch('/api/install', { 377 | method: 'POST', 378 | headers: { 379 | 'Content-Type': 'application/json' 380 | }, 381 | body: JSON.stringify({ packageName }) 382 | }); 383 | 384 | return await response.json(); 385 | } catch (error) { 386 | console.error('安装依赖失败:', error); 387 | return { success: false, message: `安装失败: ${error.message}` }; 388 | } 389 | } 390 | 391 | /** 392 | * 安装wheel文件 393 | * @param {File} wheelFile - wheel文件 394 | */ 395 | export async function installWhl(wheelFile) { 396 | const formData = new FormData(); 397 | formData.append('file', wheelFile); 398 | 399 | // 直接使用fetch处理文件上传,而不是通过apiRequest 400 | const url = `${API_BASE_URL}/install-whl`; 401 | try { 402 | const response = await fetch(url, { 403 | method: 'POST', 404 | body: formData // 不设置headers,让浏览器自动处理multipart/form-data 405 | }); 406 | 407 | const data = await response.json(); 408 | 409 | if (!response.ok) { 410 | console.error(`安装whl文件失败:`, data.message || response.statusText); 411 | return { 412 | success: false, 413 | message: data.message || `请求失败: ${response.status}` 414 | }; 415 | } 416 | 417 | return data; 418 | } catch (error) { 419 | console.error(`API错误 [install-whl]:`, error); 420 | return { 421 | success: false, 422 | message: `操作失败: ${error.message}` 423 | }; 424 | } 425 | } 426 | 427 | /** 428 | * 安装requirements.txt文件 429 | * @param {File} requirementsFile - requirements.txt文件 430 | */ 431 | export async function installRequirements(requirementsFile) { 432 | const formData = new FormData(); 433 | formData.append('file', requirementsFile); 434 | 435 | // 直接使用fetch处理文件上传,而不是通过apiRequest 436 | const url = `${API_BASE_URL}/install-requirements`; 437 | try { 438 | const response = await fetch(url, { 439 | method: 'POST', 440 | body: formData // 不设置headers,让浏览器自动处理multipart/form-data 441 | }); 442 | 443 | const data = await response.json(); 444 | 445 | if (!response.ok) { 446 | console.error(`安装requirements失败:`, data.message || response.statusText); 447 | return { 448 | success: false, 449 | message: data.message || `请求失败: ${response.status}` 450 | }; 451 | } 452 | 453 | return data; 454 | } catch (error) { 455 | console.error(`API错误 [install-requirements]:`, error); 456 | return { 457 | success: false, 458 | message: `操作失败: ${error.message}` 459 | }; 460 | } 461 | } 462 | 463 | /** 464 | * 清理PIP缓存 465 | * @returns {Promise} 响应对象 466 | */ 467 | export async function cleanPipCache() { 468 | try { 469 | const response = await fetch('/api/clean-pip-cache', { 470 | method: 'POST', 471 | headers: { 472 | 'Content-Type': 'application/json' 473 | }, 474 | body: JSON.stringify({}) // 发送空对象确保格式正确 475 | }); 476 | 477 | return await response.json(); 478 | } catch (error) { 479 | console.error('清理PIP缓存失败:', error); 480 | return { success: false, message: `清理失败: ${error.message}` }; 481 | } 482 | } 483 | 484 | /** 485 | * 检查所有依赖的最新版本 486 | * @param {Function} progressCallback - 进度回调函数 487 | * @returns {Promise} 是否完成 488 | */ 489 | export async function checkLatestVersions(progressCallback) { 490 | try { 491 | const response = await fetch('/api/check-versions', { 492 | method: 'POST', 493 | headers: { 494 | 'Content-Type': 'application/json' 495 | } 496 | }); 497 | 498 | if (!response.ok) { 499 | throw new Error(`检查版本失败: ${response.status}`); 500 | } 501 | 502 | // 处理SSE流 503 | const reader = response.body.getReader(); 504 | const decoder = new TextDecoder(); 505 | 506 | while (true) { 507 | const { value, done } = await reader.read(); 508 | if (done) break; 509 | 510 | const text = decoder.decode(value); 511 | const lines = text.split('\n').filter(line => line.trim()); 512 | 513 | for (const line of lines) { 514 | try { 515 | const data = JSON.parse(line); 516 | if (data.progress !== undefined && progressCallback) { 517 | progressCallback(data.progress); 518 | } 519 | if (data.error) { 520 | throw new Error(data.error); 521 | } 522 | } catch (e) { 523 | // 忽略无法解析的行 524 | } 525 | } 526 | } 527 | 528 | return true; 529 | } catch (error) { 530 | console.error('检查最新版本失败:', error); 531 | return false; 532 | } 533 | } 534 | 535 | /** 536 | * 获取包版本历史 537 | * @param {string} packageName - 包名称 538 | * @returns {Promise} 版本历史数据 539 | */ 540 | export async function getVersionHistory(packageName) { 541 | try { 542 | // 使用PyPI API获取版本历史,也可以替换为自定义API 543 | const response = await fetch(`https://pypi.org/pypi/${packageName}/json`); 544 | 545 | if (!response.ok) { 546 | throw new Error(`获取版本历史失败: ${response.status}`); 547 | } 548 | 549 | const data = await response.json(); 550 | 551 | // 转换数据格式 552 | const versions = Object.keys(data.releases).map(version => { 553 | const release = data.releases[version][0] || {}; 554 | return { 555 | version, 556 | date: release.upload_time ? new Date(release.upload_time).toLocaleDateString() : '未知', 557 | url: release.url || '#' 558 | }; 559 | }); 560 | 561 | // 按版本号排序,最新的在前 562 | versions.sort((a, b) => { 563 | try { 564 | return -compareVersions(a.version, b.version); 565 | } catch (e) { 566 | return 0; 567 | } 568 | }); 569 | 570 | return { 571 | name: packageName, 572 | versions: versions, 573 | latestVersion: data.info.version 574 | }; 575 | } catch (error) { 576 | console.error('获取版本历史失败:', error); 577 | throw error; 578 | } 579 | } 580 | 581 | /** 582 | * 比较两个版本号大小 583 | * @param {string} v1 - 第一个版本号 584 | * @param {string} v2 - 第二个版本号 585 | * @returns {number} 比较结果,1: v1>v2, -1: v1 v.split(/[.-]/).map(p => isNaN(p) ? p : Number(p)); 589 | const parts1 = normalize(v1); 590 | const parts2 = normalize(v2); 591 | 592 | for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { 593 | const part1 = parts1[i] === undefined ? 0 : parts1[i]; 594 | const part2 = parts2[i] === undefined ? 0 : parts2[i]; 595 | 596 | if (typeof part1 !== typeof part2) { 597 | return typeof part1 === 'number' ? -1 : 1; 598 | } 599 | 600 | if (part1 !== part2) { 601 | return part1 > part2 ? 1 : -1; 602 | } 603 | } 604 | 605 | return 0; 606 | } 607 | 608 | /** 609 | * 获取可用的Python环境 610 | * @returns {Promise} 环境信息 611 | */ 612 | export async function getPythonEnvironments() { 613 | try { 614 | return await apiRequest(`${API_BASE_URL}/python-environments`); 615 | } catch (error) { 616 | console.error('获取Python环境失败:', error); 617 | throw error; 618 | } 619 | } 620 | 621 | /** 622 | * 保存Python环境配置 623 | * @param {Object} environment - 环境配置对象 624 | * @returns {Promise} 响应结果 625 | */ 626 | export async function saveEnvironment(environment) { 627 | try { 628 | return await apiRequest(`${API_BASE_URL}/python-environments`, { 629 | method: 'POST', 630 | headers: { 631 | 'Content-Type': 'application/json' 632 | }, 633 | body: JSON.stringify(environment) 634 | }); 635 | } catch (error) { 636 | console.error('保存环境配置失败:', error); 637 | throw error; 638 | } 639 | } 640 | 641 | /** 642 | * 删除Python环境 643 | * @param {string} environmentId - 环境ID 644 | * @returns {Promise} 响应结果 645 | */ 646 | export async function deleteEnvironment(environmentId) { 647 | try { 648 | return await apiRequest(`${API_BASE_URL}/python-environments/${environmentId}`, { 649 | method: 'DELETE' 650 | }); 651 | } catch (error) { 652 | console.error('删除环境失败:', error); 653 | throw error; 654 | } 655 | } 656 | 657 | /** 658 | * 切换到指定Python环境 659 | * @param {string} environmentId - 环境ID 660 | * @returns {Promise} 响应结果 661 | */ 662 | export async function switchEnvironment(environmentId) { 663 | try { 664 | return await apiRequest(`${API_BASE_URL}/switch-environment`, { 665 | method: 'POST', 666 | headers: { 667 | 'Content-Type': 'application/json' 668 | }, 669 | body: JSON.stringify({ environmentId }) 670 | }); 671 | } catch (error) { 672 | console.error('切换环境失败:', error); 673 | throw error; 674 | } 675 | } 676 | 677 | /** 678 | * 浏览并查找可用的Python环境 679 | * @returns {Promise} 找到的环境列表 680 | */ 681 | export async function browsePythonEnvironments() { 682 | try { 683 | return await apiRequest(`${API_BASE_URL}/browse-python-env`, { 684 | method: 'POST' 685 | }); 686 | } catch (error) { 687 | console.error('浏览Python环境失败:', error); 688 | throw error; 689 | } 690 | } 691 | 692 | /** 693 | * 获取单个依赖的详细信息 694 | * @param {string} packageName - 包名 695 | * @param {boolean} forceRefresh - 是否强制刷新PyPI版本信息 696 | * @returns {Promise} 单个依赖的详细信息 697 | */ 698 | export async function getSingleDependency(packageName, forceRefresh = false) { 699 | try { 700 | const url = `${API_BASE_URL}/dependency/${encodeURIComponent(packageName)}?force_refresh=${forceRefresh}`; 701 | return await apiRequest(url); 702 | } catch (error) { 703 | console.error(`获取依赖 ${packageName} 信息失败:`, error); 704 | throw error; 705 | } 706 | } 707 | 708 | // ========== 核心业务逻辑相关函数 ========== 709 | 710 | /** 711 | * 刷新依赖列表 - 核心功能 712 | * @param {boolean} useCache - 是否使用缓存而不检查更新 713 | * @param {Function} progressCallback - 进度回调函数 714 | * @returns {Promise} 依赖列表 715 | */ 716 | export async function refreshDependencies(useCache = false, progressCallback = null) { 717 | try { 718 | console.log('刷新依赖列表...'); 719 | 720 | // 调用进度回调 721 | if (progressCallback) { 722 | progressCallback('start'); 723 | } 724 | 725 | // 尝试更新加载进度(如果在初始加载过程中) 726 | try { 727 | const { updateInitialLoadingProgress } = await import('./ui-components.js'); 728 | updateInitialLoadingProgress(40, '正在获取依赖列表...'); 729 | } catch (e) { 730 | // 忽略错误,可能不是在初始加载阶段 731 | } 732 | 733 | // 在加载新数据前先显示加载状态 734 | const { showLoadingMessage } = await import('./ui-components.js'); 735 | showLoadingMessage('刷新依赖列表...'); 736 | 737 | // 调用进度回调 - 加载阶段 738 | if (progressCallback) { 739 | progressCallback('loading'); 740 | } 741 | 742 | // 确保使用最新数据获取依赖 743 | const dependencies = await loadDependencies(useCache); 744 | 745 | // 更新进度 746 | try { 747 | const { updateInitialLoadingProgress } = await import('./ui-components.js'); 748 | updateInitialLoadingProgress(60, '依赖列表加载完成'); 749 | } catch (e) { 750 | // 忽略错误 751 | } 752 | 753 | // 调用进度回调 - 处理阶段 754 | if (progressCallback) { 755 | progressCallback('processing'); 756 | } 757 | 758 | // 强制清空UI并完全重新渲染 759 | const { renderDependencyList } = await import('./ui-components.js'); 760 | renderDependencyList(dependencies); 761 | 762 | console.log(`依赖列表已刷新,共 ${dependencies.length} 个依赖`); 763 | 764 | // 最终进度更新 765 | try { 766 | const { updateInitialLoadingProgress } = await import('./ui-components.js'); 767 | updateInitialLoadingProgress(75, '依赖列表渲染完成'); 768 | } catch (e) { 769 | // 忽略错误 770 | } 771 | 772 | // 调用进度回调 - 完成阶段 773 | if (progressCallback) { 774 | progressCallback('complete', dependencies.length); 775 | } 776 | 777 | return dependencies; 778 | } catch (error) { 779 | console.error('刷新依赖列表失败:', error); 780 | const { showNotification } = await import('./ui-components.js'); 781 | showNotification('刷新依赖列表失败,请检查网络连接', 'error'); 782 | 783 | // 调用进度回调 - 错误 784 | if (progressCallback) { 785 | progressCallback('error', error); 786 | } 787 | 788 | return []; 789 | } 790 | } 791 | 792 | /** 793 | * 刷新单个依赖信息 - 增量刷新功能 794 | * @param {string} packageName - 包名 795 | * @param {boolean} forceRefresh - 是否强制刷新PyPI版本信息 796 | * @returns {Promise} 依赖信息 797 | */ 798 | export async function refreshSingleDependency(packageName, forceRefresh = false) { 799 | try { 800 | console.log(`刷新单个依赖: ${packageName} (强制刷新: ${forceRefresh})`); 801 | 802 | // 添加加载状态表示 803 | const dependencyItem = document.querySelector(`.dependency-item[data-name="${packageName}"]`); 804 | if (dependencyItem) { 805 | dependencyItem.classList.add('refreshing'); 806 | } 807 | 808 | // 从API获取单个依赖信息 809 | const dependencyInfo = await getSingleDependency(packageName, forceRefresh); 810 | 811 | if (!dependencyInfo) { 812 | console.log(`依赖 ${packageName} 不存在或未安装`); 813 | 814 | // 移除加载状态 815 | if (dependencyItem) { 816 | dependencyItem.classList.remove('refreshing'); 817 | } 818 | 819 | return null; 820 | } 821 | 822 | // 更新DOM中的对应项 823 | const { updateDependencyItem } = await import('./ui-components.js'); 824 | updateDependencyItem(dependencyInfo); 825 | 826 | // 移除加载状态 827 | if (dependencyItem) { 828 | dependencyItem.classList.remove('refreshing'); 829 | } 830 | 831 | console.log(`依赖 ${packageName} 已刷新,版本: ${dependencyInfo.version}`); 832 | return dependencyInfo; 833 | } catch (error) { 834 | console.error(`刷新依赖 ${packageName} 失败:`, error); 835 | 836 | // 清理可能的加载状态 837 | const dependencyItem = document.querySelector(`.dependency-item[data-name="${packageName}"]`); 838 | if (dependencyItem) { 839 | dependencyItem.classList.remove('refreshing'); 840 | } 841 | 842 | return null; 843 | } 844 | } 845 | -------------------------------------------------------------------------------- /js/modules/visualization.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 依赖关系图可视化模块 - 用于可视化包依赖关系 3 | */ 4 | 5 | import { showNotification } from './ui-components.js'; 6 | 7 | // D3.js库CDN URL - 生产环境应考虑本地部署 8 | const D3_URL = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; 9 | 10 | // 存储D3库的引用 11 | let d3 = null; 12 | 13 | /** 14 | * 加载D3.js库 15 | * @returns {Promise} D3库对象 16 | */ 17 | async function loadD3() { 18 | if (d3) return d3; // 如果已加载则直接返回 19 | 20 | return new Promise((resolve, reject) => { 21 | // 检查是否已存在 22 | if (window.d3) { 23 | d3 = window.d3; 24 | return resolve(d3); 25 | } 26 | 27 | // 创建script标签动态加载 28 | const script = document.createElement('script'); 29 | script.src = D3_URL; 30 | script.async = true; 31 | 32 | script.onload = () => { 33 | d3 = window.d3; 34 | console.log('D3.js库加载成功'); 35 | resolve(d3); 36 | }; 37 | 38 | script.onerror = () => { 39 | reject(new Error('加载D3.js库失败')); 40 | }; 41 | 42 | document.head.appendChild(script); 43 | }); 44 | } 45 | 46 | /** 47 | * 依赖关系图处理类 48 | */ 49 | class DependencyGraph { 50 | constructor() { 51 | this.modal = document.getElementById('dependency-graph-modal'); 52 | this.packageNameElem = document.getElementById('graph-package-name'); 53 | this.container = document.getElementById('dependency-graph'); 54 | this.loadingElem = document.getElementById('graph-loading'); 55 | this.errorElem = document.getElementById('graph-error'); 56 | 57 | this.initialized = false; 58 | this.simulation = null; 59 | this.tooltip = null; 60 | 61 | // 初始化事件处理 62 | this.initEvents(); 63 | } 64 | 65 | /** 66 | * 初始化事件处理 67 | * @private 68 | */ 69 | initEvents() { 70 | // 当模态框关闭时停止模拟 71 | const closeBtn = this.modal.querySelector('.close-btn'); 72 | if (closeBtn) { 73 | closeBtn.addEventListener('click', () => { 74 | this.hide(); 75 | }); 76 | } 77 | 78 | // 点击模态框背景关闭 79 | this.modal.addEventListener('click', (e) => { 80 | if (e.target === this.modal) { 81 | this.hide(); 82 | } 83 | }); 84 | 85 | // 监听窗口大小变化,调整图表大小 86 | window.addEventListener('resize', () => { 87 | if (this.simulation && this.modal.style.display === 'flex') { 88 | this.resizeGraph(); 89 | } 90 | }); 91 | } 92 | 93 | /** 94 | * 显示依赖关系图 95 | * @param {string} packageName - 包名 96 | */ 97 | async show(packageName) { 98 | if (!packageName) return; 99 | 100 | // 设置包名和显示模态框 101 | this.packageNameElem.textContent = packageName; 102 | this.modal.style.display = 'flex'; 103 | 104 | // 显示加载状态,隐藏错误 105 | this.showLoading(true); 106 | this.showError(false); 107 | 108 | try { 109 | // 确保D3.js已加载 110 | await loadD3(); 111 | 112 | // 获取依赖数据 113 | const dependencyData = await this.fetchDependencyData(packageName); 114 | 115 | // 渲染图表 116 | this.renderGraph(dependencyData); 117 | 118 | } catch (error) { 119 | console.error('显示依赖关系图失败:', error); 120 | this.showError(true, error.message); 121 | showNotification(`加载依赖关系图失败: ${error.message}`, 'error'); 122 | } 123 | } 124 | 125 | /** 126 | * 隐藏依赖关系图 127 | */ 128 | hide() { 129 | this.modal.style.display = 'none'; 130 | 131 | // 停止模拟,释放资源 132 | if (this.simulation) { 133 | this.simulation.stop(); 134 | } 135 | } 136 | 137 | /** 138 | * 设置加载状态显示 139 | * @param {boolean} isLoading - 是否显示加载状态 140 | * @private 141 | */ 142 | showLoading(isLoading) { 143 | this.loadingElem.style.display = isLoading ? 'block' : 'none'; 144 | 145 | if (isLoading) { 146 | // 清空图表容器 147 | if (this.container) { 148 | this.container.innerHTML = ''; 149 | } 150 | } 151 | } 152 | 153 | /** 154 | * 设置错误状态显示 155 | * @param {boolean} hasError - 是否显示错误 156 | * @param {string} errorMsg - 错误消息 157 | * @private 158 | */ 159 | showError(hasError, errorMsg = '加载失败') { 160 | this.errorElem.style.display = hasError ? 'block' : 'none'; 161 | this.errorElem.textContent = errorMsg; 162 | } 163 | 164 | /** 165 | * 获取依赖数据 166 | * @param {string} packageName - 包名 167 | * @returns {Promise} 依赖数据对象 168 | * @private 169 | */ 170 | async fetchDependencyData(packageName) { 171 | try { 172 | // 调用真实的API端点获取依赖关系数据 173 | // 设置最大深度为2,避免数据过大影响性能 174 | const response = await fetch(`/api/dependency-graph/${packageName}?depth=2`); 175 | 176 | if (!response.ok) { 177 | // 尝试解析错误消息 178 | let errorMessage = `HTTP错误 ${response.status}`; 179 | try { 180 | const errorData = await response.json(); 181 | if (errorData.message) { 182 | errorMessage = errorData.message; 183 | } 184 | } catch (e) { 185 | console.error('解析错误响应失败:', e); 186 | } 187 | throw new Error(errorMessage); 188 | } 189 | 190 | // 解析API响应数据 191 | const responseData = await response.json(); 192 | 193 | // 检查返回的数据是否包含必要的节点和链接信息 194 | if (!responseData.success || !responseData.nodes || !responseData.links) { 195 | throw new Error('API返回的数据格式不正确'); 196 | } 197 | 198 | return { 199 | nodes: responseData.nodes, 200 | links: responseData.links 201 | }; 202 | } catch (error) { 203 | console.error('获取依赖关系数据失败:', error); 204 | 205 | // 在发生错误时返回简化的数据结构,仅包含主包节点 206 | // 这样图表仍然可以渲染,并通过错误提示告知用户问题 207 | return { 208 | nodes: [{ 209 | id: packageName, 210 | name: packageName, 211 | version: '未知', 212 | type: 'main', 213 | description: `无法加载依赖数据: ${error.message}` 214 | }], 215 | links: [] 216 | }; 217 | } 218 | } 219 | 220 | /** 221 | * 渲染依赖关系图 222 | * @param {Object} data - 依赖关系数据 223 | * @private 224 | */ 225 | renderGraph(data) { 226 | if (!data || !data.nodes || !data.links || !this.container) { 227 | this.showError(true, '无效的依赖关系数据'); 228 | return; 229 | } 230 | 231 | // 隐藏加载和错误提示 232 | this.showLoading(false); 233 | this.showError(false); 234 | 235 | // 清空容器 236 | this.container.innerHTML = ''; 237 | 238 | // 获取容器尺寸 239 | const width = this.container.clientWidth; 240 | const height = this.container.clientHeight; 241 | 242 | // 创建SVG元素 243 | const svg = d3.select(this.container) 244 | .append('svg') 245 | .attr('width', width) 246 | .attr('height', height) 247 | .attr('viewBox', [0, 0, width, height]); 248 | 249 | // 创建缩放行为 250 | const zoom = d3.zoom() 251 | .scaleExtent([0.1, 3]) 252 | .on('zoom', (event) => { 253 | g.attr('transform', event.transform); 254 | }); 255 | 256 | svg.call(zoom); 257 | 258 | // 创建主绘图组 259 | const g = svg.append('g'); 260 | 261 | // 创建连线 262 | const link = g.append('g') 263 | .selectAll('.link') 264 | .data(data.links) 265 | .enter() 266 | .append('path') 267 | .attr('class', d => `link ${d.type}`) 268 | .attr('marker-end', 'url(#arrowhead)'); 269 | 270 | // 创建节点 271 | const node = g.append('g') 272 | .selectAll('.node') 273 | .data(data.nodes) 274 | .enter() 275 | .append('g') 276 | .attr('class', 'node') 277 | .call(this.createDrag(this)); 278 | 279 | // 节点圆圈 280 | node.append('circle') 281 | .attr('r', d => d.type === 'main' ? 12 : 8) 282 | .style('fill', d => this.getNodeColor(d.type)) 283 | .style('stroke', d => d.type === 'main' ? '#c0392b' : '#2c3e50'); 284 | 285 | // 节点文本标签 286 | node.append('text') 287 | .attr('dy', d => d.type === 'main' ? -15 : -10) 288 | .attr('text-anchor', 'middle') 289 | .text(d => d.name) 290 | .style('font-weight', d => d.type === 'main' ? 'bold' : 'normal'); 291 | 292 | // 创建力模拟 293 | this.simulation = d3.forceSimulation(data.nodes) 294 | .force('link', d3.forceLink(data.links).id(d => d.id).distance(100)) 295 | .force('charge', d3.forceManyBody().strength(-200)) 296 | .force('center', d3.forceCenter(width / 2, height / 2)) 297 | .on('tick', () => { 298 | // 更新连线位置 299 | link.attr('d', this.getLinkPath); 300 | 301 | // 更新节点位置 302 | node.attr('transform', d => `translate(${d.x}, ${d.y})`); 303 | }); 304 | 305 | // 创建节点拖拽行为 306 | node.call(d3.drag() 307 | .on('start', this.dragStarted.bind(this)) 308 | .on('drag', this.dragged.bind(this)) 309 | .on('end', this.dragEnded.bind(this)) 310 | ); 311 | 312 | // 添加交互 - 鼠标悬停显示详情 313 | this.addNodeTooltip(node); 314 | 315 | // 自动初始适应宽度 316 | const initialScale = 0.8; 317 | svg.call( 318 | zoom.transform, 319 | d3.zoomIdentity 320 | .translate(width / 2, height / 2) 321 | .scale(initialScale) 322 | .translate(-width / 2, -height / 2) 323 | ); 324 | } 325 | 326 | /** 327 | * 创建拖拽行为 328 | * @param {DependencyGraph} graphObj - 图表对象实例 329 | * @returns {Function} 拖拽行为函数 330 | * @private 331 | */ 332 | createDrag(graphObj) { 333 | return d3.drag() 334 | .on('start', function(event, d) { 335 | if (!event.active) graphObj.simulation.alphaTarget(0.3).restart(); 336 | d.fx = d.x; 337 | d.fy = d.y; 338 | }) 339 | .on('drag', function(event, d) { 340 | d.fx = event.x; 341 | d.fy = event.y; 342 | }) 343 | .on('end', function(event, d) { 344 | if (!event.active) graphObj.simulation.alphaTarget(0); 345 | d.fx = null; 346 | d.fy = null; 347 | }); 348 | } 349 | 350 | /** 351 | * 拖拽开始时的处理 352 | * @param {Object} event - 事件对象 353 | * @param {Object} d - 数据对象 354 | * @private 355 | */ 356 | dragStarted(event, d) { 357 | if (!event.active) this.simulation.alphaTarget(0.3).restart(); 358 | d.fx = d.x; 359 | d.fy = d.y; 360 | } 361 | 362 | /** 363 | * 拖拽进行中的处理 364 | * @param {Object} event - 事件对象 365 | * @param {Object} d - 数据对象 366 | * @private 367 | */ 368 | dragged(event, d) { 369 | d.fx = event.x; 370 | d.fy = event.y; 371 | } 372 | 373 | /** 374 | * 拖拽结束时的处理 375 | * @param {Object} event - 事件对象 376 | * @param {Object} d - 数据对象 377 | * @private 378 | */ 379 | dragEnded(event, d) { 380 | if (!event.active) this.simulation.alphaTarget(0); 381 | d.fx = null; 382 | d.fy = null; 383 | } 384 | 385 | /** 386 | * 获取连线路径 387 | * @param {Object} d - 连线数据 388 | * @returns {string} SVG路径字符串 389 | * @private 390 | */ 391 | getLinkPath(d) { 392 | const dx = d.target.x - d.source.x; 393 | const dy = d.target.y - d.source.y; 394 | const dr = Math.sqrt(dx * dx + dy * dy); 395 | 396 | // 计算起点和终点位置,考虑节点圆圈半径 397 | const sourceRadius = d.source.type === 'main' ? 12 : 8; 398 | const targetRadius = d.target.type === 'main' ? 12 : 8; 399 | 400 | const offsetRatio = (dr === 0) ? 0 : 1 / dr; 401 | 402 | // 计算调整后的起点和终点 403 | const ux = dx * offsetRatio; 404 | const uy = dy * offsetRatio; 405 | 406 | const sourceX = d.source.x + ux * sourceRadius; 407 | const sourceY = d.source.y + uy * sourceRadius; 408 | const targetX = d.target.x - ux * targetRadius; 409 | const targetY = d.target.y - uy * targetRadius; 410 | 411 | return `M${sourceX},${sourceY}L${targetX},${targetY}`; 412 | } 413 | 414 | /** 415 | * 获取节点颜色 416 | * @param {string} type - 节点类型 417 | * @returns {string} 颜色值 418 | * @private 419 | */ 420 | getNodeColor(type) { 421 | switch(type) { 422 | case 'main': return '#e74c3c'; // 红色 423 | case 'direct': return '#3498db'; // 蓝色 424 | case 'optional': return '#95a5a6'; // 灰色 425 | default: return '#2ecc71'; // 绿色 426 | } 427 | } 428 | 429 | /** 430 | * 添加节点悬停提示框 431 | * @param {d3.Selection} nodes - 节点选择集 432 | * @private 433 | */ 434 | addNodeTooltip(nodes) { 435 | // 创建提示框元素 436 | if (!this.tooltip) { 437 | this.tooltip = d3.select('body') 438 | .append('div') 439 | .attr('class', 'node-tooltip') 440 | .style('opacity', 0); 441 | } 442 | 443 | // 鼠标悬停显示提示框 444 | nodes.on('mouseover', (event, d) => { 445 | this.tooltip.transition() 446 | .duration(200) 447 | .style('opacity', 0.9); 448 | 449 | this.tooltip.html(` 450 |

${d.name}

451 |

版本: ${d.version || '未知'}

452 |

${d.description || '没有描述'}

453 | `) 454 | .style('left', (event.pageX + 15) + 'px') 455 | .style('top', (event.pageY - 28) + 'px'); 456 | }) 457 | .on('mouseout', () => { 458 | this.tooltip.transition() 459 | .duration(500) 460 | .style('opacity', 0); 461 | }); 462 | } 463 | 464 | /** 465 | * 重新调整图表大小 466 | * @private 467 | */ 468 | resizeGraph() { 469 | const svg = d3.select(this.container).select('svg'); 470 | if (!svg.empty()) { 471 | const width = this.container.clientWidth; 472 | const height = this.container.clientHeight; 473 | 474 | svg.attr('width', width) 475 | .attr('height', height) 476 | .attr('viewBox', [0, 0, width, height]); 477 | 478 | // 更新力模拟的中心 479 | this.simulation.force('center', d3.forceCenter(width / 2, height / 2)); 480 | this.simulation.alpha(0.3).restart(); 481 | } 482 | } 483 | } 484 | 485 | // 创建依赖关系图实例 486 | const dependencyGraph = new DependencyGraph(); 487 | 488 | /** 489 | * 显示依赖关系图 490 | * @param {string} packageName - 包名 491 | */ 492 | export function showDependencyGraph(packageName) { 493 | dependencyGraph.show(packageName); 494 | } 495 | 496 | export default { 497 | showDependencyGraph 498 | }; 499 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | AI环境管理工具 - 程序入口点 3 | 4 | 此文件是应用程序的主入口点,负责初始化和启动后端服务器。 5 | 应用功能通过导入的模块实现,保持主文件的简洁性。 6 | """ 7 | 8 | # 导入必要的模块 9 | from py import api, dependency, core 10 | 11 | # 启动应用服务器 12 | if __name__ == '__main__': 13 | core.print_status("启动AI环境管理工具...", "info") 14 | core.print_status("服务将在 http://127.0.0.1:8282 上启动", "info") 15 | 16 | # 在应用启动前加载依赖描述缓存 17 | dependency.load_descriptions() 18 | 19 | # 启动Flask应用,绑定到127.0.0.1:8282 20 | api.app.run(host='127.0.0.1', port=8282, debug=False) 21 | -------------------------------------------------------------------------------- /py/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | AI环境管理工具 - Python后端模块包 3 | 4 | 这个包包含所有与后端功能相关的Python模块,采用模块化设计以提高代码的可维护性。 5 | """ 6 | 7 | # 导出主要模块,使它们可以直接从包中导入 8 | from . import core 9 | from . import dependency 10 | from . import api 11 | 12 | # 版本信息 13 | __version__ = '1.0.0' 14 | -------------------------------------------------------------------------------- /py/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | API模块 - 处理Web API路由和请求响应 3 | 4 | 此模块提供: 5 | - Flask应用和路由定义 6 | - API端点实现 7 | - 请求处理和响应格式化 8 | - 文件上传管理 9 | """ 10 | 11 | import os 12 | import sys 13 | import json 14 | import time 15 | import tempfile 16 | import shutil 17 | import uuid 18 | import threading 19 | from flask import Flask, jsonify, request, send_from_directory, Response 20 | from flask_cors import CORS 21 | import logging 22 | 23 | # 导入自定义模块 24 | from . import core 25 | from . import dependency 26 | 27 | # 创建Flask应用实例 28 | app = Flask(__name__, static_folder='..') 29 | CORS(app) # 启用跨域请求支持 30 | 31 | # 自定义日志配置 - 减少冗余日志输出 32 | class RequestFilter(logging.Filter): 33 | def filter(self, record): 34 | # 过滤掉检查描述更新的请求日志 35 | return 'GET /api/check-description-updates' not in record.getMessage() 36 | 37 | # 应用日志过滤器 38 | logging.getLogger('werkzeug').addFilter(RequestFilter()) 39 | # 设置日志级别为WARNING,减少不必要的INFO日志 40 | logging.getLogger('werkzeug').setLevel(logging.WARNING) 41 | 42 | # 通用API响应格式化 43 | def api_response(success, message=None, data=None, status_code=200): 44 | """ 45 | 格式化API响应 46 | 47 | Args: 48 | success (bool): 操作是否成功 49 | message (str, optional): 响应消息 50 | data (dict, optional): 响应数据 51 | status_code (int): HTTP状态码 52 | 53 | Returns: 54 | tuple: (Flask JSON响应, 状态码) 55 | """ 56 | response = {"success": success} 57 | if message: 58 | response["message"] = message 59 | if data: 60 | response.update(data) 61 | return jsonify(response), status_code 62 | 63 | # 根路由 - 提供静态文件 64 | @app.route('/') 65 | def index(): 66 | """提供主页HTML""" 67 | return send_from_directory('..', 'index.html') 68 | 69 | @app.route('/') 70 | def serve_static(path): 71 | """提供静态文件""" 72 | return send_from_directory('..', path) 73 | 74 | # 获取依赖列表 75 | @app.route('/api/dependencies') 76 | def get_dependencies(): 77 | """获取所有已安装的依赖信息""" 78 | try: 79 | # 检查是否使用缓存 80 | use_cache = request.args.get('useCache', 'false').lower() == 'true' 81 | 82 | # 获取依赖列表 83 | dependencies = dependency.get_all_dependencies(use_cache) 84 | 85 | return jsonify(dependencies) 86 | except Exception as e: 87 | core.print_status(f"获取依赖列表时出错: {e}", 'error') 88 | return api_response(False, f"获取依赖列表失败: {str(e)}", status_code=500) 89 | 90 | # 获取依赖当前版本 91 | @app.route('/api/dependency-current-version/') 92 | def get_dependency_current_version(dependency_name): 93 | """获取特定依赖的当前版本信息""" 94 | try: 95 | # 获取依赖信息 96 | dep_info = dependency.get_single_dependency(dependency_name) 97 | if dep_info: 98 | return api_response(True, "获取成功", {"version": dep_info['version']}) 99 | else: 100 | return api_response(False, f"找不到依赖: {dependency_name}", status_code=404) 101 | except Exception as e: 102 | core.print_status(f"获取依赖版本信息时出错: {e}", 'error') 103 | return api_response(False, f"获取依赖版本信息失败: {str(e)}", status_code=500) 104 | 105 | # 检查依赖描述更新 106 | @app.route('/api/check-description-updates') 107 | def check_description_updates(): 108 | """检查是否有新的依赖描述信息""" 109 | # 获取前端上次更新的时间戳 110 | last_update = float(request.args.get('lastUpdate', 0)) 111 | # 检查是否为环境变更请求 112 | environment_changed = request.args.get('environmentChanged', 'false').lower() == 'true' 113 | current_time = time.time() 114 | 115 | # 检查是否为首次UI加载后的请求(时间戳接近于0表示首次请求) 116 | if last_update < 1: 117 | # UI首次加载完成,触发只更新缺失描述的操作 118 | threading.Thread( 119 | target=dependency.async_update_descriptions, 120 | kwargs={'only_missing': True}, 121 | daemon=True 122 | ).start() 123 | core.print_status("UI加载完成,开始后台更新缺失的依赖描述", 'info') 124 | # 环境变更请求,只更新缺失的依赖描述 125 | elif environment_changed: 126 | # 环境已变更,触发更新所有依赖描述 127 | threading.Thread( 128 | target=dependency.async_update_descriptions, 129 | kwargs={'only_missing': True}, # 改为只更新缺失的依赖描述 130 | daemon=True 131 | ).start() 132 | core.print_status("Python环境已切换,开始更新所有的依赖描述", 'info') 133 | # 强制标记有更新 134 | if hasattr(dependency, 'last_description_update'): 135 | dependency.last_description_update = current_time 136 | 137 | # 检查是否有更新 138 | has_updates = False 139 | if current_time - last_update < 10 and hasattr(dependency, 'last_description_update'): 140 | if dependency.last_description_update > last_update: 141 | has_updates = True 142 | 143 | return jsonify({'hasUpdates': has_updates}) 144 | 145 | # 批量卸载依赖 146 | @app.route('/api/batch-uninstall', methods=['POST']) 147 | def batch_uninstall(): 148 | """批量卸载多个依赖""" 149 | data = request.json 150 | packages = data.get('packages', []) 151 | 152 | if not packages: 153 | return api_response(False, '没有选择要卸载的依赖', status_code=400) 154 | 155 | # 创建任务 156 | task_id = core.create_task('卸载', packages) 157 | 158 | # 启动后台任务执行批量卸载 159 | threading.Thread( 160 | target=dependency.batch_uninstall, 161 | args=(packages, task_id), 162 | daemon=True 163 | ).start() 164 | 165 | return api_response(True, f'正在卸载 {len(packages)} 个依赖', {'taskId': task_id}) 166 | 167 | # 安装依赖 168 | @app.route('/api/install', methods=['POST']) 169 | def install_dependency_route(): 170 | """安装依赖""" 171 | data = request.json 172 | package_name = data.get('packageName', '') 173 | 174 | if not package_name: 175 | return api_response(False, '包名称不能为空', status_code=400) 176 | 177 | # 执行安装 178 | success, message = dependency.install_dependency(package_name) 179 | 180 | if success: 181 | return api_response(True, message) 182 | else: 183 | return api_response(False, message, status_code=400) 184 | 185 | # 卸载依赖 186 | @app.route('/api/uninstall', methods=['POST']) 187 | def uninstall_dependency_route(): 188 | """卸载依赖""" 189 | data = request.json 190 | package_name = data.get('dependency', '') 191 | 192 | if not package_name: 193 | return api_response(False, '依赖名称不能为空', status_code=400) 194 | 195 | # 执行卸载 196 | success, message = dependency.uninstall_dependency(package_name) 197 | 198 | if success: 199 | return api_response(True, message) 200 | else: 201 | return api_response(False, message, status_code=400) 202 | 203 | # 更新依赖 204 | @app.route('/api/update', methods=['POST']) 205 | def update_dependency_route(): 206 | """更新依赖到最新版本""" 207 | data = request.json 208 | package_name = data.get('dependency', '') 209 | 210 | if not package_name: 211 | return api_response(False, '依赖名称不能为空', status_code=400) 212 | 213 | # 创建任务 214 | task_id = core.create_task('更新', [package_name]) 215 | 216 | # 启动后台任务执行更新 217 | threading.Thread( 218 | target=lambda: dependency.update_dependency(package_name, task_id), 219 | daemon=True 220 | ).start() 221 | 222 | return api_response(True, f'正在更新: {package_name}', {'taskId': task_id}) 223 | 224 | # 切换依赖版本 225 | @app.route('/api/switch-version', methods=['POST']) 226 | def switch_version_route(): 227 | """切换依赖版本""" 228 | data = request.json 229 | package_name = data.get('dependency', '') 230 | version = data.get('version', '') 231 | 232 | if not package_name or not version: 233 | return api_response(False, '依赖名称和版本不能为空', status_code=400) 234 | 235 | # 创建任务 236 | task_id = core.create_task('切换版本', [f"{package_name}=={version}"]) 237 | 238 | # 启动后台任务执行版本切换 239 | threading.Thread( 240 | target=lambda: dependency.switch_version(package_name, version, task_id), 241 | daemon=True 242 | ).start() 243 | 244 | return api_response(True, f'正在将 {package_name} 切换到版本 {version}', {'taskId': task_id}) 245 | 246 | # 更新所选依赖 247 | @app.route('/api/update-selected', methods=['POST']) 248 | def update_selected_route(): 249 | """更新所选依赖""" 250 | data = request.json 251 | packages = data.get('packages', []) 252 | 253 | if not packages: 254 | return api_response(False, '没有选择要更新的依赖', status_code=400) 255 | 256 | # 创建任务 257 | task_id = core.create_task('更新', packages) 258 | 259 | # 启动后台任务执行批量更新 260 | threading.Thread( 261 | target=dependency.batch_update, 262 | args=(packages, task_id), 263 | daemon=True 264 | ).start() 265 | 266 | return api_response(True, f'正在更新 {len(packages)} 个依赖', {'taskId': task_id}) 267 | 268 | # 安装wheel文件 269 | @app.route('/api/install-whl', methods=['POST']) 270 | def install_whl_route(): 271 | """安装wheel文件""" 272 | if 'file' not in request.files: 273 | return api_response(False, '没有上传文件', status_code=400) 274 | 275 | file = request.files['file'] 276 | if file.filename == '': 277 | return api_response(False, '没有选择文件', status_code=400) 278 | 279 | if not file.filename.endswith('.whl'): 280 | return api_response(False, '只支持.whl文件', status_code=400) 281 | 282 | try: 283 | # 创建临时文件 284 | temp_dir = tempfile.mkdtemp() 285 | temp_file_path = os.path.join(temp_dir, file.filename) 286 | 287 | # 保存上传的文件 288 | file.save(temp_file_path) 289 | 290 | # 创建任务ID用于跟踪进度 291 | task_id = core.create_task('安装WHL', [file.filename]) 292 | 293 | # 使用线程启动安装过程,实现异步操作 294 | def process_whl_install(): 295 | try: 296 | result = dependency.install_whl(temp_file_path, task_id) 297 | if not result: 298 | core.complete_task(task_id, [f"安装失败: {file.filename}"]) 299 | finally: 300 | # 确保临时目录被清理 301 | if os.path.exists(temp_dir): 302 | shutil.rmtree(temp_dir, ignore_errors=True) 303 | 304 | # 启动后台任务 305 | threading.Thread(target=process_whl_install, daemon=True).start() 306 | 307 | return api_response(True, f'正在安装 {file.filename},请等待...', {'taskId': task_id}) 308 | except Exception as e: 309 | # 确保清理临时文件 310 | if 'temp_dir' in locals(): 311 | shutil.rmtree(temp_dir, ignore_errors=True) 312 | core.print_status(f"处理wheel安装请求时出错: {str(e)}", 'error') 313 | return api_response(False, f'安装失败: {str(e)}', status_code=400) 314 | 315 | # 安装requirements.txt文件 316 | @app.route('/api/install-requirements', methods=['POST']) 317 | def install_requirements_route(): 318 | """安装requirements.txt文件""" 319 | if 'file' not in request.files: 320 | return api_response(False, '没有上传文件', status_code=400) 321 | 322 | file = request.files['file'] 323 | if file.filename == '': 324 | return api_response(False, '没有选择文件', status_code=400) 325 | 326 | if not file.filename.endswith('.txt'): 327 | return api_response(False, '只支持.txt文件', status_code=400) 328 | 329 | try: 330 | # 创建临时文件 331 | temp_dir = tempfile.mkdtemp() 332 | temp_file_path = os.path.join(temp_dir, 'requirements.txt') 333 | 334 | # 保存上传的文件 335 | file.save(temp_file_path) 336 | 337 | # 创建任务ID 338 | task_id = core.create_task('安装', ['从requirements.txt安装']) 339 | 340 | # 后台处理函数 341 | def process_requirements_install(): 342 | try: 343 | result = dependency.install_requirements(temp_file_path, task_id) 344 | if not result: 345 | core.complete_task(task_id, [f"安装失败: {file.filename}"]) 346 | finally: 347 | # 确保临时目录被清理 348 | if os.path.exists(temp_dir): 349 | shutil.rmtree(temp_dir, ignore_errors=True) 350 | 351 | # 启动后台任务 352 | threading.Thread(target=process_requirements_install, daemon=True).start() 353 | 354 | return api_response(True, '正在安装packages,请等待...', {'taskId': task_id}) 355 | except Exception as e: 356 | # 确保清理临时文件 357 | if 'temp_dir' in locals(): 358 | shutil.rmtree(temp_dir, ignore_errors=True) 359 | return api_response(False, f'安装失败: {str(e)}', status_code=500) 360 | 361 | # 获取任务进度 362 | @app.route('/api/task-progress/') 363 | def get_task_progress(task_id): 364 | """获取任务进度""" 365 | if task_id not in core.task_progress: 366 | return api_response(False, '任务不存在', status_code=404) 367 | 368 | return jsonify(core.task_progress[task_id]) 369 | 370 | # 清理PIP缓存 371 | @app.route('/api/clean-pip-cache', methods=['POST']) 372 | def clean_pip_cache_route(): 373 | """清理pip缓存""" 374 | # 创建任务 375 | task_id = core.create_task('清理缓存', ['pip cache']) 376 | 377 | # 启动后台任务执行缓存清理 378 | threading.Thread( 379 | target=lambda: dependency.clean_pip_cache(task_id), 380 | daemon=True 381 | ).start() 382 | 383 | return api_response(True, '正在清理PIP缓存', {'taskId': task_id}) 384 | 385 | # 检查所有依赖的最新版本 386 | @app.route('/api/check-versions', methods=['POST']) 387 | def check_all_versions(): 388 | """检查所有依赖的最新版本 - 返回SSE流以报告进度""" 389 | def generate(): 390 | try: 391 | # 获取已安装的依赖列表 392 | all_deps = dependency.get_all_dependencies(use_cache=True) 393 | 394 | # 筛选需要检查的包 - 排除系统依赖和软件依赖 395 | packages_to_check = [ 396 | dep for dep in all_deps 397 | if not dep['isSystem'] and not dep['isAppRequired'] 398 | ] 399 | 400 | # 计算检查的总数量 401 | total = len(packages_to_check) 402 | core.print_status(f"需要检查 {total} 个依赖的版本信息", 'info') 403 | 404 | # 发送初始进度 405 | yield json.dumps({"progress": 0}) + "\n" 406 | 407 | # 启动异步更新 408 | threading.Thread( 409 | target=dependency.async_update_descriptions_and_versions, 410 | daemon=True 411 | ).start() 412 | 413 | # 模拟进度报告 - 由于实际进度无法精确测量,我们使用模拟的进度报告 414 | progress_steps = [10, 25, 40, 60, 75, 90, 100] 415 | for progress in progress_steps: 416 | time.sleep(0.5) # 添加短暂延迟以模拟处理时间 417 | yield json.dumps({"progress": progress}) + "\n" 418 | if progress == 100: 419 | break 420 | 421 | except Exception as e: 422 | core.print_status(f"检查版本过程出错: {e}", 'error') 423 | yield json.dumps({"error": str(e)}) + "\n" 424 | 425 | return Response(generate(), mimetype='text/event-stream') 426 | 427 | # 获取系统信息 428 | @app.route('/api/system-info') 429 | def get_system_info(): 430 | """获取Python和pip版本信息""" 431 | try: 432 | import subprocess 433 | 434 | # 获取Python版本 435 | python_version = sys.version.split()[0] 436 | 437 | # 获取pip版本 438 | pip_version_process = subprocess.run( 439 | [sys.executable, '-m', 'pip', '--version'], 440 | capture_output=True, 441 | text=True 442 | ) 443 | pip_version = pip_version_process.stdout.split()[1] if pip_version_process.returncode == 0 else "未知" 444 | 445 | return jsonify({ 446 | 'pythonVersion': python_version, 447 | 'pipVersion': pip_version 448 | }) 449 | except Exception as e: 450 | core.print_status(f"获取系统信息失败: {e}", 'error') 451 | return jsonify({ 452 | 'pythonVersion': '未知', 453 | 'pipVersion': '未知' 454 | }), 500 455 | 456 | # 获取缓存信息 457 | @app.route('/api/cache-info') 458 | def get_cache_info(): 459 | """获取缓存信息,包括最后更新时间""" 460 | try: 461 | cache_info = core.get_cache_info() 462 | return jsonify(cache_info) 463 | except Exception as e: 464 | core.print_status(f"获取缓存信息失败: {e}", 'error') 465 | return api_response(False, f"获取缓存信息失败: {str(e)}", status_code=500) 466 | 467 | # 获取依赖分类 468 | @app.route('/api/dependency-categories') 469 | def get_dependency_categories(): 470 | """获取依赖分类信息""" 471 | try: 472 | return jsonify(core.dependency_config) 473 | except Exception as e: 474 | core.print_status(f"获取依赖分类信息失败: {e}", 'error') 475 | return api_response(False, f"获取依赖分类信息失败: {str(e)}", status_code=500) 476 | 477 | # 获取依赖关系图数据 478 | @app.route('/api/dependency-graph/') 479 | def get_dependency_graph(package_name): 480 | """获取指定包的依赖关系图数据""" 481 | try: 482 | # 获取查询参数 483 | max_depth = request.args.get('depth', default=2, type=int) # 默认深度为2层 484 | include_dev = request.args.get('dev', default='false', type=str).lower() == 'true' 485 | 486 | # 限制最大深度以避免过大的响应 487 | if max_depth > 4: 488 | max_depth = 4 489 | 490 | # 调用依赖模块获取依赖关系图数据 491 | graph_data = dependency.get_dependency_graph(package_name, max_depth, include_dev) 492 | 493 | # 返回图数据 494 | return api_response(True, data=graph_data) 495 | 496 | except Exception as e: 497 | app.logger.error(f"获取依赖关系图失败: {str(e)}", exc_info=True) 498 | return api_response(False, f"获取依赖关系图失败: {str(e)}", status_code=500) 499 | 500 | # 导入新增模块 501 | import subprocess 502 | import platform 503 | import os.path 504 | 505 | # 获取所有Python环境 506 | @app.route('/api/python-environments') 507 | def get_python_environments(): 508 | """获取所有已配置的Python环境""" 509 | try: 510 | environments = core.load_python_environments() 511 | 512 | # 如果是首次运行且没有环境,尝试添加当前环境 513 | if not environments.get("environments") and environments.get("current") is None: 514 | current_env = { 515 | "id": "system", 516 | "name": "当前Python环境", 517 | "path": sys.executable, 518 | "type": "system", 519 | "version": sys.version.split()[0] 520 | } 521 | environments["environments"] = [current_env] 522 | environments["current"] = "system" 523 | core.save_python_environments(environments) 524 | 525 | return jsonify(environments) 526 | except Exception as e: 527 | core.print_status(f"获取Python环境列表失败: {e}", 'error') 528 | return api_response(False, f"获取Python环境列表失败: {str(e)}", status_code=500) 529 | 530 | # 保存Python环境 531 | @app.route('/api/python-environments', methods=['POST']) 532 | def save_python_environment(): 533 | """新增或修改Python环境""" 534 | try: 535 | data = request.json 536 | 537 | # 加载现有环境 538 | environments = core.load_python_environments() 539 | 540 | # 生成唯一ID 541 | import uuid 542 | env_id = data.get("id") or str(uuid.uuid4()) 543 | 544 | # 验证环境路径 545 | python_path = data.get("path", "") 546 | if not python_path or not os.path.exists(python_path): 547 | return api_response(False, "Python可执行文件路径无效", status_code=400) 548 | 549 | # 获取Python版本 550 | try: 551 | version_process = subprocess.run( 552 | [python_path, "--version"], 553 | capture_output=True, 554 | text=True, 555 | check=True 556 | ) 557 | version = version_process.stdout.strip() or version_process.stderr.strip() 558 | version = version.replace("Python ", "") 559 | except Exception as e: 560 | return api_response(False, f"无法获取Python版本: {str(e)}", status_code=400) 561 | 562 | # 创建或更新环境 563 | new_env = { 564 | "id": env_id, 565 | "name": data.get("name", f"Python {version}"), 566 | "path": python_path, 567 | "type": data.get("type", "custom"), 568 | "version": version 569 | } 570 | 571 | # 更新或添加环境 572 | updated = False 573 | for i, env in enumerate(environments["environments"]): 574 | if env["id"] == env_id: 575 | environments["environments"][i] = new_env 576 | updated = True 577 | break 578 | 579 | if not updated: 580 | environments["environments"].append(new_env) 581 | 582 | # 保存更新 583 | if not core.save_python_environments(environments): 584 | return api_response(False, "保存环境配置失败", status_code=500) 585 | 586 | return api_response(True, "环境已保存", {"environment": new_env}) 587 | 588 | except Exception as e: 589 | core.print_status(f"保存Python环境失败: {e}", 'error') 590 | return api_response(False, f"保存Python环境失败: {str(e)}", status_code=500) 591 | 592 | # 删除Python环境 593 | @app.route('/api/python-environments/', methods=['DELETE']) 594 | def delete_python_environment(env_id): 595 | """删除Python环境""" 596 | try: 597 | # 加载现有环境 598 | environments = core.load_python_environments() 599 | 600 | # 检查是否是当前环境 601 | if environments.get("current") == env_id: 602 | return api_response(False, "不能删除当前使用的环境", status_code=400) 603 | 604 | # 查找和删除环境 605 | found = False 606 | for i, env in enumerate(environments["environments"]): 607 | if env["id"] == env_id: 608 | del environments["environments"][i] 609 | found = True 610 | break 611 | 612 | if not found: 613 | return api_response(False, "环境不存在", status_code=404) 614 | 615 | # 保存更新 616 | if not core.save_python_environments(environments): 617 | return api_response(False, "保存环境配置失败", status_code=500) 618 | 619 | return api_response(True, "环境已删除") 620 | 621 | except Exception as e: 622 | core.print_status(f"删除Python环境失败: {e}", 'error') 623 | return api_response(False, f"删除Python环境失败: {str(e)}", status_code=500) 624 | 625 | # 修改切换环境API 626 | @app.route('/api/switch-environment', methods=['POST']) 627 | def switch_environment(): 628 | """切换到指定的Python环境""" 629 | try: 630 | data = request.json 631 | env_id = data.get("environmentId") 632 | 633 | if not env_id: 634 | return api_response(False, "环境ID不能为空", status_code=400) 635 | 636 | # 加载环境配置 637 | environments = core.load_python_environments() 638 | 639 | # 查找目标环境 640 | target_env = None 641 | for env in environments["environments"]: 642 | if env["id"] == env_id: 643 | target_env = env 644 | break 645 | 646 | if not target_env: 647 | return api_response(False, "目标环境不存在", status_code=404) 648 | 649 | # 检查环境可执行文件是否存在 650 | python_path = target_env["path"] 651 | if not os.path.exists(python_path): 652 | return api_response(False, "Python可执行文件路径无效", status_code=400) 653 | 654 | # 检查是否是当前环境 655 | current_env_id = environments.get("current") 656 | if current_env_id == env_id: 657 | return api_response(True, "已经是当前环境", {"environment": target_env, "needsRefresh": False}) 658 | 659 | # 更新当前环境 660 | environments["current"] = env_id 661 | if not core.save_python_environments(environments): 662 | return api_response(False, "保存环境配置失败", status_code=500) 663 | 664 | # 返回成功信息,无需重启应用 665 | return api_response(True, "环境切换成功", { 666 | "requiresRestart": False, 667 | "needsRefresh": True, 668 | "environment": target_env 669 | }) 670 | 671 | except Exception as e: 672 | core.print_status(f"切换Python环境失败: {e}", 'error') 673 | return api_response(False, f"切换Python环境失败: {str(e)}", status_code=500) 674 | 675 | # 浏览Python环境 676 | @app.route('/api/browse-python-env', methods=['POST']) 677 | def browse_python_env(): 678 | """浏览并查找Python环境""" 679 | try: 680 | # 根据操作系统,搜索常见的Python安装位置 681 | os_type = platform.system().lower() 682 | potential_paths = [] 683 | 684 | if os_type == 'windows': 685 | # 检查常见Windows Python安装位置 686 | drives = ['C:', 'D:', 'E:', 'F:'] 687 | for drive in drives: 688 | # 搜索标准Python安装 689 | potential_paths.extend([ 690 | os.path.join(drive, r'Python*\python.exe'), 691 | os.path.join(drive, r'Program Files\Python*\python.exe'), 692 | os.path.join(drive, r'Program Files (x86)\Python*\python.exe'), 693 | os.path.join(drive, r'Users\*\AppData\Local\Programs\Python\Python*\python.exe'), 694 | os.path.join(drive, r'ProgramData\Anaconda*\python.exe'), 695 | os.path.join(drive, r'Users\*\anaconda*\python.exe'), 696 | os.path.join(drive, r'Users\*\miniconda*\python.exe'), 697 | os.path.join(drive, r'Users\*\Anaconda*\python.exe'), 698 | os.path.join(drive, r'Users\*\Miniconda*\python.exe'), 699 | ]) 700 | # 搜索虚拟环境 701 | potential_paths.extend([ 702 | os.path.join(drive, r'Users\*\*env*\Scripts\python.exe'), 703 | os.path.join(drive, r'*env*\Scripts\python.exe'), 704 | ]) 705 | elif os_type in ['linux', 'darwin']: # Linux or macOS 706 | # 检查常见Unix-like系统Python位置 707 | potential_paths.extend([ 708 | '/usr/bin/python*', 709 | '/usr/local/bin/python*', 710 | '/opt/anaconda*/bin/python', 711 | '/opt/miniconda*/bin/python', 712 | os.path.expanduser('~/anaconda*/bin/python'), 713 | os.path.expanduser('~/miniconda*/bin/python'), 714 | os.path.expanduser('~/.virtualenvs/*/bin/python'), 715 | os.path.expanduser('~/venv*/bin/python'), 716 | os.path.expanduser('~/*env*/bin/python'), 717 | ]) 718 | 719 | # 执行搜索并验证找到的Python路径 720 | found_environments = [] 721 | 722 | for pattern in potential_paths: 723 | try: 724 | import glob 725 | paths = glob.glob(pattern) 726 | 727 | for path in paths: 728 | if os.path.isfile(path) and os.access(path, os.X_OK): 729 | try: 730 | # 验证是否是有效的Python可执行文件 731 | version_process = subprocess.run( 732 | [path, "--version"], 733 | capture_output=True, 734 | text=True, 735 | timeout=2 # 设置超时避免挂起 736 | ) 737 | if version_process.returncode == 0: 738 | version_output = version_process.stdout.strip() or version_process.stderr.strip() 739 | if "Python" in version_output: 740 | version = version_output.replace("Python ", "").strip() 741 | 742 | # 生成环境名称 743 | dirs = path.split(os.sep) 744 | env_name = f"Python {version}" 745 | 746 | # 尝试从路径推断更好的名称 747 | for i in range(len(dirs)-2, 0, -1): 748 | if "env" in dirs[i].lower() or "conda" in dirs[i].lower() or "python" in dirs[i].lower(): 749 | env_name = f"{dirs[i]} ({version})" 750 | break 751 | 752 | # 确定环境类型 753 | env_type = "system" 754 | if "virtualenv" in path.lower() or "venv" in path.lower(): 755 | env_type = "virtualenv" 756 | elif "conda" in path.lower(): 757 | env_type = "conda" 758 | elif "portable" in path.lower(): 759 | env_type = "portable" 760 | 761 | # 添加到找到的环境列表 762 | found_environments.append({ 763 | "path": path, 764 | "version": version, 765 | "name": env_name, 766 | "type": env_type 767 | }) 768 | except Exception as e: 769 | print(f"验证Python路径 {path} 时出错: {str(e)}") 770 | except Exception as e: 771 | print(f"搜索模式 {pattern} 时出错: {str(e)}") 772 | 773 | # 去除重复项 774 | unique_environments = [] 775 | seen_paths = set() 776 | 777 | for env in found_environments: 778 | if env["path"] not in seen_paths: 779 | seen_paths.add(env["path"]) 780 | unique_environments.append(env) 781 | 782 | return api_response(True, f"找到 {len(unique_environments)} 个Python环境", { 783 | "environments": unique_environments 784 | }) 785 | 786 | except Exception as e: 787 | core.print_status(f"浏览Python环境失败: {e}", 'error') 788 | return api_response(False, f"浏览Python环境失败: {str(e)}", status_code=500) 789 | 790 | # 获取单个依赖的详细信息 791 | @app.route('/api/dependency/') 792 | def get_single_dependency(package_name): 793 | """获取单个依赖的详细信息,支持安装/卸载/更新后的增量刷新""" 794 | try: 795 | # 检查是否强制刷新PyPI缓存 796 | force_refresh = request.args.get('force_refresh', 'false').lower() == 'true' 797 | 798 | # 获取单个依赖的信息 799 | dep_info = dependency.get_single_dependency_info(package_name, force_refresh) 800 | 801 | if dep_info: 802 | return jsonify(dep_info) 803 | else: 804 | return api_response(False, f"依赖 {package_name} 未安装或不存在", status_code=404) 805 | except Exception as e: 806 | core.print_status(f"获取依赖 {package_name} 信息失败: {e}", 'error') 807 | return api_response(False, f"获取依赖信息失败: {str(e)}", status_code=500) 808 | -------------------------------------------------------------------------------- /py/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 核心模块 - 提供配置管理和通用工具函数 3 | 4 | 此模块合并了原来的config.py和utils.py,提供: 5 | - 应用程序配置管理 6 | - 缓存和设置处理 7 | - 彩色输出和状态显示 8 | - 任务管理功能 9 | - 文件操作工具 10 | - 进程输出处理 11 | """ 12 | 13 | import os 14 | import sys 15 | import time 16 | import json 17 | import subprocess 18 | import threading 19 | import re 20 | import shlex 21 | import contextlib 22 | 23 | # =========================================== 24 | # 配置管理部分 (原 config.py) 25 | # =========================================== 26 | 27 | # 配置目录路径 28 | CONFIG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'config') 29 | 30 | # 依赖描述缓存文件路径 31 | CACHE_FILE = os.path.join(CONFIG_DIR, 'dependency_cache.json') 32 | # 依赖配置文件路径 33 | DEPENDENCIES_CONFIG_FILE = os.path.join(CONFIG_DIR, 'dependencies_config.json') 34 | # 环境配置文件路径 35 | PYTHON_ENVS_FILE = os.path.join(CONFIG_DIR, 'python_environments.json') 36 | 37 | # 确保配置目录存在 38 | if not os.path.exists(CONFIG_DIR): 39 | try: 40 | os.makedirs(CONFIG_DIR) 41 | except Exception as e: 42 | print(f"创建config目录失败: {e}") 43 | 44 | # 加载依赖配置 45 | def load_dependencies_config(): 46 | """ 47 | 加载依赖配置文件,获取系统依赖、核心依赖等配置信息 48 | 49 | Returns: 50 | dict: 依赖配置字典 51 | """ 52 | if os.path.exists(DEPENDENCIES_CONFIG_FILE): 53 | try: 54 | with open(DEPENDENCIES_CONFIG_FILE, 'r', encoding='utf-8') as f: 55 | return json.load(f) 56 | except Exception as e: 57 | print(f"加载依赖配置文件失败: {e}") 58 | 59 | # 如果配置文件不存在或加载失败,返回默认配置 60 | return { 61 | "systemDependencies": [ 62 | "pip", "setuptools", "wheel", "flask", "flask-cors", "requests", "packaging" 63 | ], 64 | "dataScienceDependencies": ["numpy", "pandas"], 65 | "aiDependencies": ["transformers"], 66 | "softwareDependencies": [ 67 | "flask", "flask-cors", "requests" 68 | ] 69 | } 70 | 71 | # 加载依赖描述缓存 72 | def load_description_cache(): 73 | """ 74 | 加载缓存的依赖描述信息 75 | 76 | Returns: 77 | dict: 依赖描述缓存字典 78 | """ 79 | if os.path.exists(CACHE_FILE): 80 | try: 81 | with open(CACHE_FILE, 'r', encoding='utf-8') as f: 82 | content = f.read().strip() 83 | if content: # 检查文件是否为空 84 | return json.loads(content) 85 | else: 86 | print("依赖描述缓存文件为空,将使用空缓存") 87 | return {} 88 | except json.JSONDecodeError: 89 | print("依赖描述缓存文件格式无效,将使用空缓存") 90 | return {} 91 | except Exception as e: 92 | print(f"加载缓存文件失败: {e}") 93 | return {} 94 | return {} 95 | 96 | # 保存依赖描述到缓存 97 | def save_description_cache(dependency_descriptions): 98 | """ 99 | 保存依赖描述到缓存文件 100 | 101 | Args: 102 | dependency_descriptions (dict): 依赖描述字典 103 | 104 | Returns: 105 | bool: 是否保存成功 106 | """ 107 | try: 108 | with open(CACHE_FILE, 'w', encoding='utf-8') as f: 109 | json.dump(dependency_descriptions, f, ensure_ascii=False, indent=2) 110 | return True 111 | except Exception as e: 112 | print(f"保存缓存文件失败: {e}") 113 | return False 114 | 115 | # 获取缓存信息 116 | def get_cache_info(): 117 | """ 118 | 获取缓存状态信息 119 | 120 | Returns: 121 | dict: 缓存信息字典 122 | """ 123 | latest_versions_cache_file = os.path.join(CONFIG_DIR, 'latest_versions_cache.json') 124 | 125 | cache_info = { 126 | 'exists': False, 127 | 'last_update': None 128 | } 129 | 130 | if os.path.exists(latest_versions_cache_file): 131 | try: 132 | with open(latest_versions_cache_file, 'r', encoding='utf-8') as f: 133 | data = json.load(f) 134 | if isinstance(data, dict) and 'last_update' in data: 135 | cache_info['exists'] = True 136 | cache_info['last_update'] = data['last_update'] 137 | elif isinstance(data, dict) and len(data) > 0: 138 | # 旧版缓存格式兼容处理 139 | cache_info['exists'] = True 140 | # 使用文件修改时间作为最后更新时间 141 | cache_info['last_update'] = os.path.getmtime(latest_versions_cache_file) 142 | except Exception as e: 143 | print(f"读取缓存信息失败: {e}") 144 | 145 | return cache_info 146 | 147 | # 检查缓存是否有效 148 | def is_cache_valid(): 149 | """ 150 | 检查缓存是否在有效期内 151 | 152 | Returns: 153 | bool: 缓存是否有效 154 | """ 155 | cache_info = get_cache_info() 156 | 157 | if not cache_info['exists'] or not cache_info['last_update']: 158 | print('缓存不存在或无效,将执行版本检查') 159 | return False 160 | 161 | # 检查缓存是否在有效期内 162 | last_update_time = cache_info['last_update'] * 1000 # 转为毫秒 163 | current_time = time.time() * 1000 164 | cache_age_in_days = (current_time - last_update_time) / (1000 * 60 * 60 * 24) 165 | 166 | print(f"缓存年龄: {cache_age_in_days:.2f}天, 缓存{'有效' if cache_age_in_days < 1 else '失效'}") 167 | 168 | return cache_age_in_days < 1 169 | 170 | # 加载Python环境配置 171 | def load_python_environments(): 172 | """ 173 | 加载已保存的Python环境配置 174 | 175 | Returns: 176 | dict: Python环境配置字典,包含路径和类型 177 | """ 178 | default_environments = { 179 | "environments": [], 180 | "current": None 181 | } 182 | 183 | if os.path.exists(PYTHON_ENVS_FILE): 184 | try: 185 | with open(PYTHON_ENVS_FILE, 'r', encoding='utf-8') as f: 186 | return json.load(f) 187 | except Exception as e: 188 | print(f"加载Python环境配置文件失败: {e}") 189 | 190 | # 如果配置文件不存在或加载失败,创建默认配置文件 191 | save_python_environments(default_environments) 192 | return default_environments 193 | 194 | # 保存Python环境配置 195 | def save_python_environments(environments): 196 | """ 197 | 保存Python环境配置到文件 198 | 199 | Args: 200 | environments (dict): Python环境配置 201 | 202 | Returns: 203 | bool: 是否保存成功 204 | """ 205 | try: 206 | with open(PYTHON_ENVS_FILE, 'w', encoding='utf-8') as f: 207 | json.dump(environments, f, ensure_ascii=False, indent=2) 208 | return True 209 | except Exception as e: 210 | print(f"保存Python环境配置失败: {e}") 211 | return False 212 | 213 | # 添加获取当前活动Python环境路径函数 214 | def get_active_python_executable(): 215 | """ 216 | 获取当前活动的Python可执行文件路径 217 | 218 | Returns: 219 | str: Python可执行文件路径 220 | """ 221 | environments = load_python_environments() 222 | current_env_id = environments.get('current') 223 | 224 | if not current_env_id: 225 | # 默认使用当前Python 226 | return sys.executable 227 | 228 | # 查找当前环境信息 229 | for env in environments.get('environments', []): 230 | if env.get('id') == current_env_id and os.path.exists(env.get('path', '')): 231 | return env.get('path') 232 | 233 | # 如果找不到有效的环境,返回当前Python 234 | return sys.executable 235 | 236 | # =========================================== 237 | # 工具函数部分 (原 utils.py) 238 | # =========================================== 239 | 240 | # 彩色输出配置 241 | class Colors: 242 | HEADER = '\033[95m' 243 | BLUE = '\033[94m' 244 | GREEN = '\033[92m' 245 | WARNING = '\033[93m' 246 | FAIL = '\033[91m' 247 | ENDC = '\033[0m' 248 | BOLD = '\033[1m' 249 | 250 | # 全局任务进度跟踪字典 251 | task_progress = {} 252 | 253 | # 格式化中文提示输出 254 | def print_status(message, status='info'): 255 | """ 256 | 打印带颜色的状态消息 257 | 258 | Args: 259 | message (str): 要打印的消息 260 | status (str): 消息类型 ('success', 'error', 'warning', 'start', 'info') 261 | """ 262 | if status == 'success': 263 | print(f"{Colors.GREEN}【成功】{message}{Colors.ENDC}") 264 | elif status == 'error': 265 | print(f"{Colors.FAIL}【错误】{message}{Colors.ENDC}") 266 | elif status == 'warning': 267 | print(f"{Colors.WARNING}【警告】{message}{Colors.ENDC}") 268 | elif status == 'start': 269 | print(f"{Colors.BLUE}【开始】{message}{Colors.ENDC}") 270 | else: 271 | print(f"{Colors.BOLD}【信息】{message}{Colors.ENDC}") 272 | 273 | # 封装安全的文件读写操作 274 | @contextlib.contextmanager 275 | def safe_open_file(file_path, mode='r', encoding='utf-8'): 276 | """ 277 | 安全打开文件的上下文管理器 278 | 279 | Args: 280 | file_path (str): 文件路径 281 | mode (str): 打开模式 282 | encoding (str): 文件编码 283 | 284 | Yields: 285 | file: 打开的文件对象 286 | """ 287 | try: 288 | f = open(file_path, mode, encoding=encoding) 289 | yield f 290 | except Exception as e: 291 | print(f"文件操作失败 [{file_path}]: {e}") 292 | raise 293 | finally: 294 | if 'f' in locals(): 295 | f.close() 296 | 297 | # 通用任务管理函数 298 | def create_task(task_type, items): 299 | """ 300 | 创建一个新任务 301 | 302 | Args: 303 | task_type (str): 任务类型 304 | items (list): 任务项列表 305 | 306 | Returns: 307 | str: 任务ID 308 | """ 309 | import uuid 310 | task_id = str(uuid.uuid4()) 311 | task_progress[task_id] = { 312 | 'progress': 0, 313 | 'total': len(items), 314 | 'current': 0, 315 | 'status': 'running', 316 | 'message': f'准备{task_type}', 317 | 'errors': [] 318 | } 319 | return task_id 320 | 321 | def update_task_progress(task_id, current, message=None): 322 | """ 323 | 更新任务进度 324 | 325 | Args: 326 | task_id (str): 任务ID 327 | current (int): 当前进度 328 | message (str, optional): 进度消息 329 | """ 330 | if task_id not in task_progress: 331 | return 332 | 333 | task = task_progress[task_id] 334 | task['current'] = current 335 | task['progress'] = int((current / task['total']) * 100) if task['total'] > 0 else 0 336 | 337 | if message: 338 | task['message'] = message 339 | 340 | def complete_task(task_id, errors=None): 341 | """ 342 | 完成任务 343 | 344 | Args: 345 | task_id (str): 任务ID 346 | errors (list, optional): 错误列表 347 | """ 348 | if task_id not in task_progress: 349 | return 350 | 351 | task_progress[task_id]['status'] = 'completed' 352 | task_progress[task_id]['progress'] = 100 353 | task_progress[task_id]['message'] = '处理完成' 354 | 355 | if errors: 356 | task_progress[task_id]['errors'] = errors 357 | 358 | # 安排任务清理 359 | schedule_task_cleanup(task_id) 360 | 361 | def schedule_task_cleanup(task_id, delay=86400): 362 | """ 363 | 安排任务清理 364 | 365 | Args: 366 | task_id (str): 任务ID 367 | delay (int): 延迟时间(秒) 368 | """ 369 | def cleanup_task(): 370 | time.sleep(delay) # 默认24小时后清理 371 | if task_id in task_progress: 372 | del task_progress[task_id] 373 | 374 | threading.Thread(target=cleanup_task, daemon=True).start() 375 | 376 | # 通用批量处理功能 377 | def process_batch_operation(packages, task_id, operation_func, should_skip_func=None, skip_message=""): 378 | """ 379 | 通用批量操作处理 380 | 381 | Args: 382 | packages (list): 包名称列表 383 | task_id (str): 任务ID 384 | operation_func (callable): 操作函数 385 | should_skip_func (callable, optional): 判断是否应该跳过的函数 386 | skip_message (str, optional): 跳过时的消息 387 | """ 388 | errors = [] 389 | total = len(packages) 390 | 391 | for index, pkg in enumerate(packages): 392 | try: 393 | # 检查是否应该跳过 394 | if should_skip_func and should_skip_func(pkg): 395 | errors.append(f"{pkg}: {skip_message}") 396 | continue 397 | 398 | # 更新进度 399 | update_task_progress( 400 | task_id, 401 | index + 1, 402 | f'处理 {pkg} ({index + 1}/{total})' 403 | ) 404 | 405 | # 执行操作 406 | operation_func(pkg) 407 | 408 | except Exception as e: 409 | errors.append(f"{pkg}: {str(e)}") 410 | 411 | # 完成任务 412 | complete_task(task_id, errors) 413 | 414 | def stream_process_output(cmd, task_id, package_name=None): 415 | """ 416 | 流式处理命令输出并更新进度条 417 | 418 | Args: 419 | cmd (list or str): 命令及参数 420 | task_id (str): 任务ID 421 | package_name (str, optional): 包名称 422 | 423 | Returns: 424 | bool: 命令是否成功执行 425 | """ 426 | try: 427 | # 设置初始进度 428 | update_task_progress(task_id, 0, f'开始处理 {package_name or cmd}...') 429 | 430 | # 分割命令为参数列表 431 | if isinstance(cmd, str): 432 | cmd = shlex.split(cmd) 433 | 434 | # 启动进程并捕获实时输出 435 | process = subprocess.Popen( 436 | cmd, 437 | stdout=subprocess.PIPE, 438 | stderr=subprocess.STDOUT, 439 | text=True, 440 | bufsize=1, 441 | universal_newlines=True 442 | ) 443 | 444 | # 用于跟踪进度的变量 445 | download_started = False 446 | current_percent = 0 447 | current_status = f'准备 {package_name or "任务"}...' 448 | 449 | # 正则表达式用于匹配不同类型的进度输出 450 | progress_pattern = re.compile(r'(\d+)%\|.*\| (\d+(\.\d+)?)([kKmMgG]i?B)/(\d+(\.\d+)?)([kKmMgG]i?B)') 451 | simple_percent_pattern = re.compile(r'(\d+)%') 452 | step_pattern = re.compile(r'(Building|Collecting|Installing|Processing)\s+([^\s]+)') 453 | 454 | # 处理每一行输出 455 | for line in iter(process.stdout.readline, ''): 456 | line = line.strip() 457 | 458 | # 跳过空行 459 | if not line: 460 | continue 461 | 462 | # 打印原始输出 463 | print(line) 464 | 465 | # 尝试解析进度信息 466 | progress_match = progress_pattern.search(line) 467 | simple_match = simple_percent_pattern.search(line) 468 | step_match = step_pattern.search(line) 469 | 470 | # 如果找到进度百分比信息 (例如: "45%|████ | 3.6/8.1MB") 471 | if progress_match: 472 | percent = int(progress_match.group(1)) 473 | downloaded_str = progress_match.group(2) 474 | downloaded_unit = progress_match.group(4) 475 | total_str = progress_match.group(5) 476 | total_unit = progress_match.group(7) 477 | 478 | # 更新进度信息 479 | download_started = True 480 | current_percent = percent 481 | current_status = f"下载中: {downloaded_str}{downloaded_unit}/{total_str}{total_unit} ({percent}%)" 482 | 483 | # 创建命令行进度条 484 | progress_bar = create_cli_progress_bar(percent) 485 | print(f"\r{progress_bar} {current_status}", end="") 486 | 487 | # 更新任务进度 488 | update_task_progress(task_id, percent, current_status) 489 | 490 | # 简单百分比匹配 (例如: "Installing... 30%") 491 | elif simple_match and not progress_match: 492 | percent = int(simple_match.group(1)) 493 | if percent > current_percent: # 只更新更高的进度 494 | current_percent = percent 495 | current_status = f"处理中: {percent}%" 496 | # 创建命令行进度条 497 | progress_bar = create_cli_progress_bar(percent) 498 | print(f"\r{progress_bar} {current_status}", end="") 499 | 500 | # 更新任务进度 501 | update_task_progress(task_id, percent, current_status) 502 | 503 | # 如果找到步骤信息 (例如: "Collecting numpy" 或 "Installing collected packages: pip") 504 | elif step_match: 505 | action = step_match.group(1) 506 | package = step_match.group(2) 507 | 508 | # 根据不同步骤设置不同的进度 509 | if action == "Collecting" and not download_started: 510 | update_task_progress(task_id, 10, f"收集依赖包: {package}") 511 | current_status = f"收集依赖包: {package}" 512 | elif action == "Building": 513 | update_task_progress(task_id, max(30, current_percent), f"构建包: {package}") 514 | current_status = f"构建包: {package}" 515 | elif action == "Installing": 516 | update_task_progress(task_id, max(70, current_percent), f"安装包: {package}") 517 | current_status = f"安装包: {package}" 518 | 519 | # 对于没有明确进度信息的行,至少提供一些状态更新 520 | else: 521 | # 检查是否是某些特殊状态 522 | if "Successfully installed" in line: 523 | current_percent = 100 524 | installed_packages = line.replace("Successfully installed", "").strip() 525 | current_status = f"成功安装: {installed_packages}" 526 | update_task_progress(task_id, 100, current_status) 527 | print(f"\r{create_cli_progress_bar(100)} {current_status}") 528 | elif "Requirement already satisfied" in line: 529 | package_info = line.replace("Requirement already satisfied:", "").strip().split()[0] 530 | current_status = f"依赖已满足: {package_info}" 531 | update_task_progress(task_id, max(50, current_percent), current_status) 532 | 533 | # 等待进程完成并获取返回码 534 | return_code = process.wait() 535 | 536 | # 确保进度条显示完成 537 | if current_percent < 100 and return_code == 0: 538 | update_task_progress(task_id, 100, "处理完成") 539 | print(f"\r{create_cli_progress_bar(100)} 处理完成") 540 | 541 | # 确保光标移到下一行 542 | print() 543 | 544 | # 返回进程状态 545 | return return_code == 0 546 | 547 | except Exception as e: 548 | print(f"处理命令输出时出错: {e}") 549 | update_task_progress(task_id, 100, f"处理出错: {str(e)}") 550 | return False 551 | 552 | def create_cli_progress_bar(percent, width=30): 553 | """ 554 | 创建命令行ASCII进度条 555 | 556 | Args: 557 | percent (int): 进度百分比 558 | width (int): 进度条宽度 559 | 560 | Returns: 561 | str: ASCII进度条字符串 562 | """ 563 | completed = int(width * percent / 100) 564 | remaining = width - completed 565 | return f"[{'#' * completed}{' ' * remaining}] {percent}%" 566 | 567 | # =========================================== 568 | # 模块初始化 569 | # =========================================== 570 | 571 | # 加载所有配置 572 | dependency_config = load_dependencies_config() 573 | SYSTEM_DEPENDENCIES = dependency_config.get('systemDependencies', []) 574 | CORE_DEPENDENCIES = dependency_config.get('dataScienceDependencies', []) 575 | AI_MODEL_DEPENDENCIES = dependency_config.get('aiDependencies', []) 576 | APP_DEPENDENCIES = dependency_config.get('softwareDependencies', []) 577 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | flask-cors 3 | requests 4 | packaging 5 | importlib-metadata; python_version < "3.8" 6 | pipdeptree -------------------------------------------------------------------------------- /启动.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | title AI环境管理工具 - 启动器 3 | color 0B 4 | setlocal 5 | 6 | echo. 7 | echo ===================================== 8 | echo AI环境管理工具 - 启动器 9 | echo 作者: B站Dontdrunk 10 | echo ===================================== 11 | echo. 12 | 13 | echo [选项菜单] 14 | echo. 15 | echo [1] 启动程序 - 启动AI环境管理工具 16 | echo [2] 检查环境 - 检测并安装必要的Python依赖 17 | echo [3] 软件介绍 - 查看软件功能和使用方法 18 | echo [4] 退出软件 - 退出启动器 19 | echo. 20 | echo ===================================== 21 | echo. 22 | 23 | set /p "UserChoice=请输入选项 [1-4]: " 24 | echo. 25 | 26 | if "%UserChoice%"=="1" goto StartApplication 27 | if "%UserChoice%"=="2" goto CheckEnvironment 28 | if "%UserChoice%"=="3" goto ShowIntroduction 29 | if "%UserChoice%"=="4" goto ExitApplication 30 | 31 | echo 无效的选择,请输入1-4之间的数字 32 | timeout /t 2 >nul 33 | goto ShowMenu 34 | 35 | :CheckPythonInstalled 36 | where python >nul 2>nul 37 | if %errorlevel% neq 0 ( 38 | echo 错误: 未检测到Python安装。请安装Python 3.7+后重试。 39 | echo 按任意键退出... 40 | pause >nul 41 | exit /b 1 42 | ) 43 | 44 | python -c "print('Python找到了')" >nul 2>nul 45 | if %errorlevel% neq 0 ( 46 | echo 错误: Python无法正常运行,请检查安装。 47 | echo 按任意键退出... 48 | pause >nul 49 | exit /b 1 50 | ) 51 | 52 | goto :EOF 53 | 54 | :StartApplication 55 | echo 正在启动AI环境管理工具,请稍候... 56 | echo. 57 | 58 | :: 检查Python是否安装 59 | call :CheckPythonInstalled 60 | if %errorlevel% neq 0 exit /b 61 | 62 | echo 初始化程序组件... 63 | 64 | :: 检查main.py是否存在 65 | if not exist "main.py" ( 66 | echo 错误: main.py文件不存在,请确保在正确目录下运行。 67 | echo 当前目录: %CD% 68 | pause 69 | exit /b 1 70 | ) 71 | 72 | :: 修改后的端口检测逻辑:只检测LISTENING状态的端口 73 | netstat -ano | findstr ":8282.*LISTENING" >nul 74 | if %errorlevel% equ 0 ( 75 | echo 警告: 端口8282已被占用,可能是程序已经在运行。 76 | echo 如需重启,请先关闭已运行的实例。 77 | pause 78 | exit /b 79 | ) 80 | 81 | :: 在后台启动浏览器(不等待其关闭) 82 | echo 正在启动浏览器,请稍候... 83 | start "" http://127.0.0.1:8282 84 | 85 | :: 直接在当前窗口运行Python程序 86 | echo 正在启动服务,按Ctrl+C可以停止服务... 87 | echo --------------------------------------------------------------- 88 | 89 | :: 运行Python程序并等待其完成 - 不使用start命令 90 | python main.py 91 | 92 | :: Python程序结束后返回主菜单 93 | echo. 94 | echo --------------------------------------------------------------- 95 | echo 服务已停止。按任意键返回主菜单... 96 | pause >nul 97 | goto ShowMenu 98 | 99 | :CheckEnvironment 100 | echo 环境检查和依赖安装 101 | echo. 102 | 103 | :: 检查Python是否安装 104 | call :CheckPythonInstalled 105 | if %errorlevel% neq 0 exit /b 106 | 107 | echo ✓ 已检测到Python安装 108 | 109 | :: 检查pip 110 | echo 正在检查pip... 111 | python -m pip --version >nul 2>nul 112 | if %errorlevel% neq 0 ( 113 | echo 未检测到pip。正在尝试安装... 114 | python -m ensurepip 115 | if %errorlevel% neq 0 ( 116 | echo 安装pip失败。请手动安装pip后重试。 117 | pause 118 | goto :EOF 119 | ) 120 | ) 121 | echo ✓ pip已安装 122 | 123 | :: 更新pip 124 | echo 正在更新pip到最新版本... 125 | python -m pip install --upgrade pip >nul 2>nul 126 | echo ✓ pip已更新到最新版本 127 | 128 | :: 检查并安装必要的依赖 129 | echo 正在检查和安装必要的依赖... 130 | echo. 131 | 132 | echo 安装flask... 133 | python -m pip install flask 134 | echo 安装flask-cors... 135 | python -m pip install flask-cors 136 | echo 安装requests... 137 | python -m pip install requests 138 | echo 安装packaging... 139 | python -m pip install packaging 140 | 141 | python -c "import sys; print(sys.version_info < (3,8))" | findstr "True" >nul 142 | if %errorlevel% equ 0 ( 143 | echo 检测到Python版本低于3.8,安装兼容性依赖... 144 | python -m pip install importlib-metadata 145 | ) 146 | 147 | echo. 148 | echo 环境检查完成!所有必要的依赖已安装。 149 | echo 按任意键返回... 150 | pause >nul 151 | goto ShowMenu 152 | 153 | :ShowIntroduction 154 | echo AI环境管理工具 - 软件介绍 155 | echo. 156 | echo 这是一个用于管理Python AI开发环境的工具,提供直观的Web界面,帮助您: 157 | echo. 158 | echo • 查看已安装的Python依赖包 159 | echo • 一键安装、更新和卸载依赖 160 | echo • 切换依赖的特定版本 161 | echo • 批量管理多个依赖 162 | echo • 搜索和筛选依赖 163 | echo • 上传wheel文件或requirements.txt进行安装 164 | echo. 165 | echo 主要功能: 166 | echo 1. 全面的依赖概览 - 清晰展示依赖信息,标记系统和应用依赖 167 | echo 2. 高效的依赖管理 - 安装、更新、卸载和版本切换 168 | echo 3. 优化的用户体验 - 搜索、筛选和主题切换 169 | echo. 170 | echo 使用方法: 171 | echo 1. 从主菜单选择"启动程序" 172 | echo 2. 浏览器会自动打开Web界面 173 | echo 3. 或手动访问 http://127.0.0.1:8282 174 | echo. 175 | echo 开发者:B站Dontdrunk 176 | echo. 177 | echo 按任意键返回... 178 | pause >nul 179 | goto ShowMenu 180 | 181 | :ExitApplication 182 | echo 正在退出AI环境管理工具... 183 | echo. 184 | 185 | :: 检查是否有运行中的程序实例并关闭 186 | tasklist /fi "imagename eq python.exe" | find "python.exe" >nul 187 | if %errorlevel% equ 0 ( 188 | echo 检测到运行中的Python实例,正在尝试关闭... 189 | taskkill /f /im python.exe /fi "windowtitle eq AI*" >nul 2>nul 190 | if %errorlevel% equ 0 ( 191 | echo ✓ 已关闭运行中的程序实例 192 | ) else ( 193 | echo × 无法关闭程序实例 194 | ) 195 | ) 196 | 197 | echo. 198 | echo 感谢使用AI环境管理工具! 199 | echo 再见! 200 | 201 | timeout /t 3 >nul 202 | exit /b 0 203 | 204 | :: 主程序入口 205 | :ShowMenu 206 | cls 207 | echo. 208 | echo ===================================== 209 | echo AI环境管理工具 - 启动器 210 | echo 作者: B站Dontdrunk 211 | echo ===================================== 212 | echo. 213 | 214 | echo [选项菜单] 215 | echo. 216 | echo [1] 启动程序 - 启动AI环境管理工具 217 | echo [2] 检查环境 - 检测并安装必要的Python依赖 218 | echo [3] 软件介绍 - 查看软件功能和使用方法 219 | echo [4] 退出软件 - 退出启动器 220 | echo. 221 | echo ===================================== 222 | echo. 223 | 224 | set /p "UserChoice=请输入选项 [1-4]: " 225 | echo. 226 | 227 | if "%UserChoice%"=="1" goto StartApplication 228 | if "%UserChoice%"=="2" goto CheckEnvironment 229 | if "%UserChoice%"=="3" goto ShowIntroduction 230 | if "%UserChoice%"=="4" goto ExitApplication 231 | 232 | echo 无效的选择,请输入1-4之间的数字 233 | timeout /t 2 >nul 234 | goto ShowMenu 235 | 236 | :: 启动主程序 237 | call :ShowMenu 238 | --------------------------------------------------------------------------------