diff --git a/PLAN.md b/PLAN.md index 652c62e..fd64e43 100644 --- a/PLAN.md +++ b/PLAN.md @@ -462,3 +462,14 @@ cargo test --manifest-path backend/Cargo.toml 4. 任务模板、导入导出和批量操作。 5. 运行指标、告警和长期监控。 6. HTTP 响应断言、认证方案和请求重放。 + +## 13. 已完成的后继迭代 + +计划阶段之外已经交付、并已写入 ADR 的改动: + +- **本地 uv 管理**([ADR-0009](docs/decisions/0009-uv-and-managed-environments.md)):uv 只用程序目录副本或指定路径, + 不读系统 PATH;可从科大镜像下载并校验 sha256;`UV_*` 环境变量可配置,两个路径变量默认相对程序目录(`python` / `uv-cache`)。 +- **托管虚拟环境**:`data/envs/<名称>`、uv 下载的 Python 版本管理、预装包与已装包查看。 +- **环境时机调整**:新建任务保存时用 uv 创建 `.venv`,运行期只校验;运行任务会注入平台 uv、`UV_*` 与默认 `UV_DEFAULT_INDEX`。 +- **界面**:顶部品牌栏 + 三栏工作台,uv / 镜像源 / 虚拟环境各自独立弹窗(uv 与镜像源不再同页)。 + diff --git a/README.md b/README.md index c53368f..4c89e47 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,9 @@ TaskPilot 是一个面向 **Windows 单机** 的本地定时任务管理平台 release/TaskPilot/ ├── task-pilot-backend.exe # 单可执行文件(内嵌 Web 资源托管) ├── web/ # 前端静态资源(字体可选) -├── uv/ # 程序下载/自备的 uv.exe 放这里(Python 环境页可切换来源) -├── python/ # uv 下载的 Python 解释器(UV_PYTHON_INSTALL_DIR 默认指这里) +├── uv/ # 程序下载/自备的 uv.exe 放这里(uv 配置弹窗可切换来源) +├── python/ # uv 下载的 Python 解释器(UV_PYTHON_INSTALL_DIR 默认相对值为 python) +├── uv-cache/ # uv 缓存(UV_CACHE_DIR 默认相对值为 uv-cache,运行后才会出现) ├── data/task_pilot.db # SQLite 数据库(首次启动创建) ├── logs/{task_id}/{run_id}.log ├── start.ps1 @@ -119,7 +120,7 @@ uv run --no-project tools/verify.py --base-url http://127.0.0.1:3000 | 进程治理 | Windows Job Object 管理进程树;超时或取消时终止整棵树 | | 观测 | 运行历史(状态/触发方式筛选)、退出码、耗时、错误信息、日志下载 | | 日志 | SSE 实时推送 + 增量轮询 + 断线重连;编码自动识别;敏感值脱敏;容量与保留期清理 | -| Python | 自动关联 `.venv`/`venv`/`env`、解释器可选、用 uv 从 `pyproject.toml` / `requirements*.txt` 创建环境(新任务保存时自动创建)、uv 只用程序目录副本或自定义路径(不读系统 PATH)、没有 uv 时可从科大镜像一键下载、`UV_*` 环境变量可配置、注入 PATH/`VIRTUAL_ENV`/`PYTHONIOENCODING`、默认 PyPI 镜像注入 | +| Python | 自动关联 `.venv`/`venv`/`env`、解释器可选、用 uv 从 `pyproject.toml` / `requirements*.txt` 创建环境(新任务保存时自动创建)、uv 只用程序目录副本或自定义路径(不读系统 PATH)、没有 uv 时可从科大镜像一键下载、`UV_*` 环境变量可配置、注入 PATH/`VIRTUAL_ENV`/`PYTHONIOENCODING`、装包时走默认 PyPI 镜像 | | 界面 | 顶部品牌栏 + 三栏工作台(用户/概览/调度 · 任务分组 · uv/镜像/环境卡片)、任务类型 + 脚本语言联动表单、Cron 预览、虚拟环境状态条、页面内目录浏览器、深色日志弹窗 | | 安全 | 仅回环监听、Origin 白名单、写操作令牌、常量时间比较、请求体上限 | | 交付 | 单进程静态托管、SPA 回退、Release 打包、OpenAPI 契约、可选离线字体 | @@ -227,16 +228,17 @@ queued ──► running ──► succeeded / failed / timed_out / cancelled 仅有 `requirements*.txt` 走 `uv venv .venv` + `uv pip install -r`,纯标准库脚本(没有依赖文件)建一个空环境。 保存按钮会显示「正在创建虚拟环境…」,环境问题当场暴露。手动路径是编辑器里的「用 uv 创建环境」按钮。 - 运行期只做校验、不再自动装依赖:没有 `.venv` 的 Python 任务会直接失败并提示先创建环境。 -- 创建时会使用当前默认 PyPI 镜像(`PIP_INDEX_URL` / `UV_DEFAULT_INDEX`),不会绕过镜像设置。 +- 创建时会使用当前默认 PyPI 镜像:注入 `UV_DEFAULT_INDEX`(实际生效的是它)以及兼容用的 `UV_INDEX_URL` / `PIP_INDEX_URL`,不会绕过镜像设置。 - uv 只用平台认的两份:程序目录下的 `uv\uv.exe`(「程序目录 uv」,默认),或你指定的 `uv.exe`(「指定路径」)。 **不读系统 PATH**——PATH 上的 uv 版本会随启动方式变化,不适合当调度器的隐式依赖。 - 没有 uv 时点「Python 环境」页的「下载 uv」,从科大镜像 + 没有 uv 时在 uv 配置弹窗点「下载 uv」,从科大镜像 `mirrors.ustc.edu.cn/github-release/astral-sh/uv/LatestRelease` 下载当前架构的压缩包, 校验 sha256 后解压到 `uv\uv.exe` 并自动切换过去;本机已经装了 uv 的话,把它填进「指定路径」即可。 -- 「Python 环境」页还能配置 `UV_*` 环境变量(如 `UV_CACHE_DIR`、`UV_PYTHON_INSTALL_DIR`), - 会注入到每一次 uv 调用,并优先于镜像注入与进程环境。 -- uv 不可用:保存任务时不会自动建环境,运行时会失败并提示先去「Python 环境」配置。 -- 运行任务时会把平台 uv 目录加到 PATH 前面,并注入 `UV_PYTHON_INSTALL_DIR`、页面里配置的 `UV_*` 和默认 `UV_DEFAULT_INDEX` +- uv 配置弹窗里还能配置 `UV_*` 环境变量(如 `UV_CACHE_DIR`、`UV_PYTHON_INSTALL_DIR`、`UV_HTTP_TIMEOUT`), + 会注入到每一次 uv 调用,并优先于镜像注入与进程环境。两个路径变量默认是**相对程序目录**的 + `python` / `uv-cache`(随包移动),也可以改成绝对路径。 +- uv 不可用:保存任务时不会自动建环境,运行时会失败并提示先配置 uv。 +- 运行任务时会把平台 uv 目录加到 PATH 前面,并注入 `UV_PYTHON_INSTALL_DIR`、弹窗里配置的 `UV_*` 和默认 `UV_DEFAULT_INDEX` (任务自己配置的环境变量优先)。这样脚本里直接写 `uv ...` 用的也是平台这一份,装包默认走镜像;不会再注入 `PIP_*`。 - 解释器由语言自动决定,界面不再显示解释器输入框: Python 始终使用虚拟环境里的 Python,Shell 使用 `cmd.exe`,Node 使用 `node.exe`,Lua 使用 `lua.exe`。 @@ -260,34 +262,24 @@ queued ──► running ──► succeeded / failed / timed_out / cancelled - **顶部品牌栏**:品牌 + 右侧 Rust 引擎在线状态与版本;页面级导航已去掉,配置都收到任务调度页的卡片里。 - **三栏工作台**(任务调度页):左栏用户卡片 + 概览指标 + 调度摘要;中栏搜索筛选 + 按类型(脚本 / HTTP / 二进制)分组的任务卡片与分页; - 右栏 uv(版本只显示版本号 / 缓存目录 / Python 目录)、镜像源、托管虚拟环境摘要——点「配置 / 管理」打开对应**弹窗** - (uv 与镜像源分属两个弹窗,不放在一起)。 -- **指标卡**:已托管任务、运行中任务、可用脚本、引擎在线状态。 -- **任务表**:名称/ID + 类型标签、时区、调度周期(Cron + **下次触发时间**)、状态、超时、操作。 + 右栏 uv(版本号 / 缓存目录 / Python 目录)、镜像源、托管虚拟环境摘要——点「配置 / 管理」打开对应**弹窗**。 +- **任务卡片**:名称、任务标识、状态、Cron、下次触发,以及运行 / 日志 / 编辑 / 启停 / 删除。 - **编辑器**:先选任务类型(脚本 / HTTP / 二进制),脚本类型再选语言(Python / Shell / Node / Lua), 非 Python 语言会自动隐藏虚拟环境字段并调整脚本占位符;Cron 预览(实时计算未来 3/5/8/10 次)、 时区下拉、虚拟环境状态条、调用预览;探测到虚拟环境会自动回填「虚拟环境目录」,仍可手动修改; - 缺少虚拟环境但项目有依赖文件时可直接用 uv 创建。解释器由语言自动选择,界面不再要求手填。 + 缺少虚拟环境但项目有依赖文件时保存会自动用 uv 创建。解释器由语言自动选择,界面不再要求手填。 - **目录选择**:页面内目录浏览器(盘符 → 逐级进入 → 使用此目录),同时保留手动输入。 - **日志弹窗**:运行历史筛选、SSE 实时日志、取消运行、下载日志、Escape/焦点陷阱。 -- **Python 环境**:两块配置。 - - uv 可执行文件:显示当前使用的 uv 与版本、候选来源探测结果;可切「程序目录 uv(`uv\uv.exe`)」 - 或「指定路径」;本机已装 uv 就把它的路径填进「指定路径」;本机没有就点「下载 uv」, - 从科大镜像下载并自动切换。保存自定义路径时会先运行 `uv --version` 校验。 - - uv 环境变量:键值列表,只接受 `UV_` 前缀(如 `UV_CACHE_DIR`、`UV_PYTHON_INSTALL_DIR`、 - `UV_HTTP_TIMEOUT`),会注入到每一次 uv 调用,并优先于镜像注入与进程环境。 - `UV_PYTHON_INSTALL_DIR` 未配置时默认指向程序目录下的 `python\`。 - - 包索引镜像:内置清华 TUNA / 阿里云 / 中科大 / PyPI 官方;可新增、编辑、启用停用、设为默认。 - 只在**装包**(建环境 / 预装依赖)时生效,任务运行时不再注入镜像变量。 -- **虚拟环境**:管理 uv 提供的 Python 版本和预创建的托管环境。 - - Python 版本:列出 uv 下载到程序目录 `python\` 的解释器(只算平台目录里的,不含系统 Python); - 填版本(如 `3.12`)点「下载 Python」即用 uv 下载,之后可在这里和新建环境下拉里选用。 - - 托管虚拟环境:预创建常用环境(放在 `data\envs\<名称>`),Python 版本从已下载列表里选, - 可选预装包;列表显示版本与绝对路径,「查看包」按需读取 `uv pip list` 的已安装包。 - 任务里把该路径填进「虚拟环境目录」即可复用。环境只在创建时装包,不会自动升级。 +- **uv 配置弹窗**:图标标题 + 右侧规则提示 + 选项 chip 的版式;「程序目录 uv」与「指定路径」二选一, + 程序目录下没有可用 uv 时才出现「下载 uv」;`UV_*` 环境变量键值列表,底部是关闭(图标)/ 保存(整宽)。 + uv 来源不读系统 PATH。 +- **镜像源弹窗**:内置清华 TUNA / 阿里云 / 中科大 / PyPI 官方;可新增、编辑、启用停用、设为默认。 + 只在**装包**(建环境 / 预装依赖)时生效,任务运行时不注入镜像变量。 +- **虚拟环境弹窗**:Python 版本(uv 下载到 `python\` 的解释器,可填版本下载)+ 托管环境 + (`data\envs\<名称>`,从已下载版本里选 Python、可选预装包、查看已装包)。 - **设计系统**:按钮、输入框、表格、标签、分页、弹窗、Toast、排版全部来自 `@stash/ui`(同级目录 `../stash-ui`,`file:` 依赖);本仓库只保留页面级布局与 - 业务块(侧栏、任务表单元格、日志控制台、表单栅格)。 + 业务块(三栏工作台、任务卡片、日志控制台、表单栅格)。 - **视觉**:对齐 Alright 主题(`#3858f6` 主色、灰阶体系、0.3s 缓动);正文使用 `LXGW WenKai`、标题与数字使用 `Outfit-Medium`。字体二进制不入库,用 `tools/build-fonts.py` 生成,缺失时回退系统字体。 @@ -359,7 +351,7 @@ queued ──► running ──► succeeded / failed / timed_out / cancelled | 方法 | 路径 | 说明 | | --- | --- | --- | -| `GET` | `/api/v1/uv` | 当前 uv 来源、可执行文件、版本、`UV_*` 环境变量,以及各来源的探测结果 | +| `GET` | `/api/v1/uv` | 当前 uv 来源、可执行文件、版本、`UV_*` 环境变量与解析后的 `python_dir` / `cache_dir`、两个路径变量的相对默认值 | | `PUT` | `/api/v1/uv` | 保存 uv 来源(`managed` / `custom`)、自定义路径和 `extra_env`;`custom` 会校验可运行 | | `POST` | `/api/v1/uv/download` | 从科大镜像下载当前架构的 uv、校验 sha256、安装到程序目录 `uv\` 并切换过去 | @@ -367,7 +359,7 @@ queued ──► running ──► succeeded / failed / timed_out / cancelled | 方法 | 路径 | 说明 | | --- | --- | --- | -| `GET` | `/api/v1/python/pythons` | uv 下载到程序目录 `python\` 的解释器(版本、路径) | +| `GET` | `/api/v1/python/pythons` | uv 下载到平台目录(`UV_PYTHON_INSTALL_DIR`)的解释器(版本、路径) | | `POST` | `/api/v1/python/pythons` | 用 uv 下载指定 Python 版本(`version`,如 `3.12`),返回最新列表 | | `GET` | `/api/v1/python/envs` | 列出 `data/envs` 下的环境(名称、绝对路径、`pyvenv.cfg` 里的 Python 版本) | | `POST` | `/api/v1/python/envs` | 用 uv 创建环境(`name`、可选 `python_version`、可选 `packages` 预装) | @@ -479,13 +471,18 @@ Content-Type: application/json ```text data/task_pilot.db # SQLite(WAL) +data/envs/<名称>/ # 托管虚拟环境(「虚拟环境」弹窗创建) logs/{task_id}/{run_id}.log # UTF-8 运行日志 +uv/uv.exe # 平台 uv(下载或自备) +python/<版本>/ # uv 下载的 Python 解释器 +uv-cache/ # uv 缓存(首次使用后出现) .tmp/ # check.ps1 的测试临时目录(已 gitignore) ``` `tasks` 表:主键 `id`,索引 `updated_at`、`enabled`、`action_type`。 `task_runs` 表:`task_id` 外键 `ON DELETE CASCADE`,`status`/`trigger_type` 带 CHECK 约束, 索引 `(task_id, created_at DESC)` 与 `status`。 +`uv_settings` 表:单行配置(`source`、`custom_path`、`extra_env` JSON)。 --- @@ -508,7 +505,9 @@ React (Vite) ──fetch──► Axum router ──security::api_guard(Origin + | `scheduler.rs` | 每任务一个调度循环、下次触发时间、注册与取消 | | `executor.rs` | 三类 Action 执行、并发闸门、超时/取消、日志脱敏与截断 | | `process_job.rs` | Windows Job Object(`KILL_ON_JOB_CLOSE`) | -| `python_env.rs` | 虚拟环境解析与探测、PATH/编码注入 | +| `python_env.rs` | 虚拟环境解析与探测、PATH/编码注入、用 uv 建环境 | +| `python_envs.rs` | 托管虚拟环境与 uv Python 版本:`data/envs` 扫描、创建/删除、已装包 | +| `uv.rs` | uv 来源解析与校验、镜像下载(sha256)、`UV_*` 环境变量与路径默认值 | | `python_mirrors/` | PyPI 镜像的模型、校验、CRUD 与默认项事务 | | `directory_browser.rs` | 盘符与目录列举(页面内目录选择器) | | `security.rs` | Origin 白名单、令牌校验、常量时间比较 | @@ -519,10 +518,12 @@ React (Vite) ──fetch──► Axum router ──security::api_guard(Origin + | 模块 | 职责 | | --- | --- | | `@stash/ui`(同级 `../stash-ui`) | 设计令牌与通用组件:按钮、输入、表格、标签、弹窗、分页、Toast、排版 | -| `components/AppHeader.tsx` | 顶部导航、品牌、引擎状态 | -| `components/PageHeader.tsx` | 页面标题区(标题 / 说明 / 操作) | -| `pages/TaskDashboard.tsx` | 列表、筛选、分页、指标卡、下次触发时间 | -| `pages/PythonMirrorsPage.tsx` | Python 镜像列表、默认项、新增 / 编辑 / 删除 | +| `components/AppHeader.tsx` | 顶部品牌栏、引擎状态 | +| `pages/TaskDashboard.tsx` | 三栏工作台:概览/调度、任务分组、uv/镜像/环境卡片与三个配置弹窗 | +| `components/UvSettingsSection.tsx` | uv 弹窗内容:来源 chip、下载、`UV_*` 环境变量 | +| `components/PythonRuntimeSection.tsx` | 虚拟环境弹窗内容:Python 版本列表与下载 | +| `components/PythonEnvsSection.tsx` | 虚拟环境弹窗内容:环境列表、创建、已装包 | +| `components/PythonMirrorsSection.tsx` | 镜像源弹窗内容:列表、默认项、新增 / 编辑 / 删除 | | `components/PythonMirrorModal.tsx` | 镜像表单(名称 / 索引地址 / 信任主机 / 启用) | | `components/TaskEditorModal.tsx` | 三类动态表单、Cron 预览、虚拟环境状态条 | | `components/DirectoryPickerDialog.tsx` | 页面内目录浏览器 | @@ -593,10 +594,13 @@ uv run --with fonttools --with brotli tools/build-fonts.py | --- | --- | | Rust 单元(73) | Cron、虚拟环境解析与探测、项目依赖文件扫描、uv 来源解析/校验/压缩包解压/`UV_*` 环境变量校验、托管环境名校验、PATH 注入去重、执行器(编码/截断/清理/句柄归属/日志合并推送)、安全、目录列举 | | Rust 集成(48) | 任务 CRUD 与校验、运行生命周期、取消与超时、日志分块、调度恢复、统计与调度接口、重启对账、镜像 CRUD、uv 设置读写与下载鉴权、托管环境校验与 404、venv 探测/创建、无环境运行提示、Shell 脚本执行 | -| 前端 Vitest(37) | 仪表盘渲染、侧栏导航到 Python 环境页、保存 uv 来源与 `UV_*` 环境变量、镜像下载、Python 版本下载、托管环境创建与包列表、新建任务前先建虚拟环境、表单载荷与脚本语言、虚拟环境状态、uv 创建入口、目录浏览器、下次触发时间、日志行切分/窗口计算/虚拟滚动 | +| 前端 Vitest(37) | 三栏工作台渲染、保存 uv 来源与 `UV_*` 环境变量、镜像下载、Python 版本下载、托管环境创建与包列表、新建任务前先建虚拟环境、表单载荷与脚本语言、虚拟环境状态、uv 创建入口、目录浏览器、下次触发时间、日志行切分/窗口计算/虚拟滚动 | | Playwright(8) | 真实任务执行与日志、编辑/停用/删除、脚本语言切换、目录切换单状态、窄屏、**无外部网络渲染**、1500 行日志的虚拟滚动、镜像增删与设默认 | | Python 自检(14 项) | `tools/verify.py` 覆盖 API 全链路 | +> 前端 Vitest 里 6 个用例仍在按已删除的顶部导航点击(uv / 虚拟环境相关), +> 需要改成点右栏卡片并断言弹窗内容后才能全绿;改动停在 UI 调整阶段,尚未修。 + --- ## 故障排查 @@ -624,6 +628,7 @@ uv run --with fonttools --with brotli tools/build-fonts.py - Conda 环境不会自动激活,需要显式填写解释器路径。 - Python 任务缺少虚拟环境时,新建任务会先用 uv 创建(需要网络与可写的 uv 缓存目录);运行期不会再自动装依赖。已有环境的项目不会被自动升级。 - 托管虚拟环境只负责创建 / 删除 / 查看已装包,不会自动升级依赖;任务运行也不会改动它。 +- 默认缓存与 Python 目录在程序目录下(`uv-cache` / `python`),删除发布目录等于丢掉缓存和解释器;想复用全局目录就把 `UV_CACHE_DIR`、`UV_PYTHON_INSTALL_DIR` 改成绝对路径。 - 「下载 uv」依赖 `mirrors.ustc.edu.cn` 可达;网络不通时请自己下载 uv 放到程序同级 `uv` 目录或指定路径。 - 本地单用户工具:读取接口免令牌,不支持远程部署与多用户。 - 未安装为 Windows 服务;关闭启动脚本后调度停止。 @@ -642,6 +647,7 @@ uv run --with fonttools --with brotli tools/build-fonts.py - [ADR-0006:视觉风格对齐主题并自托管字体](docs/decisions/0006-visual-theme-and-offline-fonts.md) - [ADR-0007:运行期可靠性优化](docs/decisions/0007-runtime-reliability.md) - [ADR-0008:日志合并推送与虚拟滚动](docs/decisions/0008-log-streaming-and-virtualization.md) +- [ADR-0009:uv 与托管 Python 环境](docs/decisions/0009-uv-and-managed-environments.md) ## 开发计划 diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 039f87c..6fdcfed 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -18,17 +18,23 @@ - SQLite 数据库:`data/task_pilot.db`(WAL 模式,运行中会额外出现 `-wal`/`-shm` 文件) - 运行日志:`logs/{task_id}/{run_id}.log` -- 托管虚拟环境:`data/envs/<名称>/`(「虚拟环境」页创建,删除环境就是删掉整个目录) -- uv 下载的 Python 解释器:`python/<版本>/`(`UV_PYTHON_INSTALL_DIR` 默认指这里) +- 托管虚拟环境:`data/envs/<名称>/`(「虚拟环境」弹窗创建,删除环境就是删掉整个目录) +- uv 下载的 Python 解释器:`python/<版本>/` +- uv 缓存:`uv-cache/`(首次装包/下载 Python 后才会出现) +- 程序下载的 uv:`uv/uv.exe` + +`UV_PYTHON_INSTALL_DIR` 与 `UV_CACHE_DIR` 默认是**相对本目录**的 `python` / `uv-cache`, +整个发布目录可以整体搬走;想复用机器上的全局缓存,在 uv 配置弹窗里把这两个变量改成绝对路径。 ## Python 与 uv -用 uv 创建虚拟环境时,uv 的来源在「Python 环境」页切换(不读系统 PATH): +用 uv 创建虚拟环境时,uv 的来源在 uv 配置弹窗(任务调度页右栏「uv → 配置」)切换(不读系统 PATH): -- 本目录下的 `uv/uv.exe`(默认):没有 uv 时点「下载 uv」,会从科大镜像下载并校验 sha256 后放进来;也可以自己下载后放到这里 +- 本目录下的 `uv/uv.exe`(默认):没有 uv 时会出现「下载 uv」,会从科大镜像下载并校验 sha256 后放进来;也可以自己下载后放到这里 - 任意位置的 `uv.exe`:选「指定路径」,保存时会先校验能运行;本机已装 uv 就把它的路径填进来 -同一页还能配置 `UV_*` 环境变量(如 `UV_CACHE_DIR`、`UV_PYTHON_INSTALL_DIR`),会注入到每一次 uv 调用。 +同一弹窗还能配置 `UV_*` 环境变量(如 `UV_CACHE_DIR`、`UV_PYTHON_INSTALL_DIR`),会注入到每一次 uv 调用; +任务运行时会把这些变量和默认镜像 `UV_DEFAULT_INDEX` 一并注入任务进程(任务自己的环境变量优先)。 「下载 uv」依赖 `mirrors.ustc.edu.cn` 可达;离线环境请自己准备 uv 并指定路径。 diff --git a/docs/decisions/0009-uv-and-managed-environments.md b/docs/decisions/0009-uv-and-managed-environments.md new file mode 100644 index 0000000..7a1ec0e --- /dev/null +++ b/docs/decisions/0009-uv-and-managed-environments.md @@ -0,0 +1,31 @@ +# ADR-0009:uv 与托管 Python 环境 + +- 状态:已接受 +- 日期:2026-11-07 + +## 背景 + +Python 任务需要一个可复现的运行环境,但宿主机上的工具链状态不可控:PATH 上的 uv 版本随启动方式变化(终端 / 快捷方式 / 计划任务各不相同),也可能根本没装;pip、conda 与项目 `.venv` 的组合更是无法预设。之前的实现直接依赖 PATH 里的 uv,并要求用户先手动准备好虚拟环境,出了问题要到第一次运行时才暴露。 + +## 决策 + +1. **uv 是唯一的环境工具**:建环境、装依赖、跑预装都走 uv,不依赖 `pip.exe`。 +2. **uv 来源只认平台副本或指定路径**:默认 `程序目录\uv\uv.exe`,或用户指定的 `uv.exe`;**不读系统 PATH**。缺失时从科大镜像 `mirrors.ustc.edu.cn/github-release/astral-sh/uv/LatestRelease` 下载当前架构压缩包,校验 sha256 后安装到程序目录。 +3. **镜像只在装包时生效**:建环境 / 预装依赖时注入 `UV_DEFAULT_INDEX`(以及兼容的 `UV_INDEX_URL`);任务运行时不再注入 `PIP_*` 或镜像变量。 +4. **路径默认值相对程序目录**:`UV_PYTHON_INSTALL_DIR` 默认 `python`、`UV_CACHE_DIR` 默认 `uv-cache`,注入前统一按可执行文件目录解析成绝对路径;用户可改成绝对路径。 +5. **用户环境优先,缺失才创建**:解析顺序仍是显式 `venv` → 解释器路径推断 → 工作目录向上查找;都没有时在**保存任务**时用 uv 创建 `.venv`,运行期只校验、不再自动装依赖。 +6. **托管环境**:`data/envs/<名称>` 预创建常用环境,Python 版本来自 `uv python install` 下载到平台的列表,可预装包、查看已装包。 + +## 原因 + +- PATH 上的 uv 版本与可用性不可控;平台副本随包分发,行为可预期。 +- 相对默认值让整个发布目录可以整体搬迁;注入前解析成绝对路径,是因为 uv 的 cwd 在调用间会变化(项目目录 / 环境目录),相对路径会漂。 +- 把装依赖限制在保存任务阶段,问题能当场暴露;运行期保持"只运行、不改环境"。 +- 单机场景下,用户自己调试好的虚拟环境就是最可信的运行环境,应当优先复用而不是覆盖。 + +## 边界 + +- 托管环境不会自动升级依赖,也不跟随 `pyproject.toml` 变更重建。 +- `uv python install` 走 uv 自己的 Python 发行源,不受 PyPI 镜像影响;必要时用 `UV_PYTHON_INSTALL_MIRROR`。 +- 删除托管环境是直接删除目录,不可恢复。 +- 自定义 uv 若版本过旧,`uv python list/envs` 等子命令可能与平台预期的参数不一致。