- README:三栏工作台与弹窗说明、uv 路径默认值(python / uv-cache)、镜像只在装包时生效、API 与数据布局、测试现状注记 - RELEASE:补 uv-cache 与 uv 目录、配置入口改为弹窗、运行时注入说明 - 新增 ADR-0009:uv 与托管 Python 环境的决策与边界 - PLAN:补记已完成的后继迭代
655 lines
34 KiB
Markdown
655 lines
34 KiB
Markdown
# TaskPilot
|
||
|
||
TaskPilot 是一个面向 **Windows 单机** 的本地定时任务管理平台:React 提供任务管理与观测界面,Rust 负责 Cron 调度、进程治理、运行日志与本地 API。
|
||
|
||
- 单用户、单实例,只监听 `127.0.0.1`
|
||
- 三类任务:Python/Shell 脚本、HTTP 请求、二进制进程
|
||
- SQLite 持久化,重启后任务与历史仍在
|
||
- 运行日志实时推送(SSE),按窗口合并事件;断线自动回退轮询,长日志用虚拟滚动展示
|
||
- Python 包镜像管理:内置常用 PyPI 源,默认镜像在任务运行时注入 `PIP_INDEX_URL` / `UV_INDEX_URL`
|
||
- 前端零外部网络依赖:样式与组件来自同级的 `stash-ui` 设计系统(`file:` 依赖,构建后随包自带);界面字体可选,未生成时自动回退系统字体
|
||
|
||
> 根目录的 `task_pilot.html` 只是最初的单文件视觉原型,正式入口是 React 应用。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [快速开始](#快速开始)
|
||
- [功能总览](#功能总览)
|
||
- [任务模型](#任务模型)
|
||
- [调度与执行语义](#调度与执行语义)
|
||
- [日志与编码](#日志与编码)
|
||
- [Python 与虚拟环境](#python-与虚拟环境)
|
||
- [Web 界面](#web-界面)
|
||
- [安全模型](#安全模型)
|
||
- [配置项](#配置项)
|
||
- [API 参考](#api-参考)
|
||
- [输入校验](#输入校验)
|
||
- [数据与文件布局](#数据与文件布局)
|
||
- [架构与模块](#架构与模块)
|
||
- [开发与测试](#开发与测试)
|
||
- [故障排查](#故障排查)
|
||
- [已知限制](#已知限制)
|
||
- [架构决策记录](#架构决策记录)
|
||
|
||
---
|
||
|
||
## 快速开始
|
||
|
||
### 方式一:打包版(推荐)
|
||
|
||
```powershell
|
||
.\scripts\build-release.ps1 # 构建前端 + Release 可执行文件 + 可分发目录
|
||
.\release\TaskPilot\start.ps1 # 启动并访问 http://127.0.0.1:3000
|
||
```
|
||
|
||
`start.ps1 -OpenBrowser` 会自动打开浏览器。产物目录结构:
|
||
|
||
```text
|
||
release/TaskPilot/
|
||
├── task-pilot-backend.exe # 单可执行文件(内嵌 Web 资源托管)
|
||
├── web/ # 前端静态资源(字体可选)
|
||
├── 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
|
||
└── README.md # 即 docs/RELEASE.md
|
||
```
|
||
|
||
### 方式二:本地开发
|
||
|
||
```powershell
|
||
# 前端依赖 @stash/ui(file:../../stash-ui),首次使用先在组件库构建 dist
|
||
npm --prefix ..\stash-ui install
|
||
npm --prefix ..\stash-ui run build
|
||
|
||
pnpm --dir frontend install
|
||
cargo run --manifest-path backend/Cargo.toml --bin task-pilot-backend # 终端一:API,127.0.0.1:3000
|
||
pnpm --dir frontend dev # 终端二:http://127.0.0.1:5173
|
||
```
|
||
|
||
Vite 把 `/api` 代理到后端。改后端端口时(`TASK_PILOT_PORT`)需同步修改
|
||
`frontend/vite.config.ts` 的代理目标;打包版同源,不需要改前端。
|
||
|
||
`@stash/ui` 走 `file:` 依赖(pnpm 会复制一份构建产物):组件库重新 `npm run build`
|
||
之后,需要再执行一次 `pnpm --dir frontend install` 才会同步新的 `dist`。
|
||
|
||
### 方式三:用 uv 跑官方示例与自检
|
||
|
||
仓库自带一个 uv 管理的演示任务和一个端到端自检脚本:
|
||
|
||
```powershell
|
||
# 1) 在示例目录创建虚拟环境(Python 3.11)
|
||
cd samples\python-task
|
||
uv venv
|
||
cd ..\..
|
||
|
||
# 2) 启动一个临时后端(任意终端)
|
||
cargo run --manifest-path backend/Cargo.toml
|
||
|
||
# 3) 运行 14 项端到端自检
|
||
uv run --no-project tools/verify.py --base-url http://127.0.0.1:3000
|
||
```
|
||
|
||
自检会真实创建任务、立即运行、读回日志,校验:健康检查、令牌、目录列举、
|
||
虚拟环境探测、创建/调度注册、运行成功、中文日志、`VIRTUAL_ENV` 注入、
|
||
无替换字符、统计口径、删除任务。
|
||
|
||
```text
|
||
[OK ] venv probe links the demo environment status=linked path=.venv\Scripts\python.exe
|
||
[OK ] task is registered in the schedule next_run_at=2026-10-25T19:00:00Z
|
||
[OK ] run succeeded status=succeeded exit=0
|
||
[OK ] log contains Chinese output
|
||
14/14 checks passed
|
||
```
|
||
|
||
`--keep` 保留任务,`--working-dir` 可换成任意含 `.venv` 的目录。
|
||
|
||
---
|
||
|
||
## 功能总览
|
||
|
||
| 分组 | 能力 |
|
||
| --- | --- |
|
||
| 任务管理 | 增删改查、启用/停用、按 ID/名称搜索、按启用状态与 Action 类型筛选、分页 |
|
||
| 调度 | 5/6 位 Cron(支持秒级、别名、月份/星期名)、IANA 时区、启动恢复调度、下次触发时间 |
|
||
| 执行 | 手动立即运行;同一任务重叠触发记为 `skipped`;启动时对账遗留运行 |
|
||
| 进程治理 | Windows Job Object 管理进程树;超时或取消时终止整棵树 |
|
||
| 观测 | 运行历史(状态/触发方式筛选)、退出码、耗时、错误信息、日志下载 |
|
||
| 日志 | SSE 实时推送 + 增量轮询 + 断线重连;编码自动识别;敏感值脱敏;容量与保留期清理 |
|
||
| Python | 自动关联 `.venv`/`venv`/`env`、解释器可选、用 uv 从 `pyproject.toml` / `requirements*.txt` 创建环境(新任务保存时自动创建)、uv 只用程序目录副本或自定义路径(不读系统 PATH)、没有 uv 时可从科大镜像一键下载、`UV_*` 环境变量可配置、注入 PATH/`VIRTUAL_ENV`/`PYTHONIOENCODING`、装包时走默认 PyPI 镜像 |
|
||
| 界面 | 顶部品牌栏 + 三栏工作台(用户/概览/调度 · 任务分组 · uv/镜像/环境卡片)、任务类型 + 脚本语言联动表单、Cron 预览、虚拟环境状态条、页面内目录浏览器、深色日志弹窗 |
|
||
| 安全 | 仅回环监听、Origin 白名单、写操作令牌、常量时间比较、请求体上限 |
|
||
| 交付 | 单进程静态托管、SPA 回退、Release 打包、OpenAPI 契约、可选离线字体 |
|
||
|
||
---
|
||
|
||
## 任务模型
|
||
|
||
### 字段
|
||
|
||
| 字段 | 说明 |
|
||
| --- | --- |
|
||
| `id` | 稳定标识,`[A-Za-z0-9_-]`,1–64 字符;同时决定日志目录名 |
|
||
| `name` | 展示名,1–120 字符 |
|
||
| `cron` | 5 或 6 位表达式,例:`0 0 2 * * *`、`*/30 * * * * *`、`@daily` |
|
||
| `timezone` | IANA 时区,例:`Asia/Shanghai` |
|
||
| `enabled` | 是否注册调度 |
|
||
| `timeout_secs` | 1–86400;超时终止整个进程树 |
|
||
| `concurrency_policy` | 目前仅 `skip`(同任务重叠触发被跳过) |
|
||
| `retry_policy` | `max_attempts` ≤ 10、`delay_secs` ≤ 3600;**当前只持久化,不自动重试** |
|
||
| `action` | 见下 |
|
||
|
||
### 三类 Action
|
||
|
||
```jsonc
|
||
// 脚本(language 省略时按 interpreter 推断,旧任务兼容)
|
||
{ "type": "script", "working_dir": "D:\\Projects\\sync", "venv": null,
|
||
"language": "python", "interpreter": "python.exe", "script": "main.py",
|
||
"args": ["--mode", "full"], "env": { "PYTHONUNBUFFERED": "1" } }
|
||
|
||
// Shell:cmd.exe /C run.bat;Node/Lua 同理
|
||
{ "type": "script", "working_dir": "D:\\Projects\\ops", "language": "shell",
|
||
"interpreter": "cmd.exe", "script": "run.bat", "args": [], "env": {} }
|
||
|
||
// HTTP
|
||
{ "type": "http", "url": "https://api.example.com/health", "method": "GET",
|
||
"headers": { "X-Token": "..." }, "body": null, "expected_status": [200, 204] }
|
||
|
||
// 进程
|
||
{ "type": "process", "executable": "C:\\Tools\\agent.exe",
|
||
"working_dir": "C:\\Tools", "args": ["--serve"], "env": {} }
|
||
```
|
||
|
||
### 运行状态机
|
||
|
||
```text
|
||
queued ──► running ──► succeeded / failed / timed_out / cancelled
|
||
└────────────────► skipped (同一任务已有运行实例)
|
||
```
|
||
|
||
终态不可再取消;进程被强杀后,下次启动会把遗留的 `queued`/`running`
|
||
统一标记为 `cancelled`,并写入「TaskPilot 进程重启,运行被中断」。
|
||
|
||
---
|
||
|
||
## 调度与执行语义
|
||
|
||
- **三类任务都参与调度**。启用即注册,停用即取消;后端重启后从数据库恢复注册。
|
||
- 每个任务一个独立 Tokio 循环:计算下次触发 → 等待 → 调用执行器。
|
||
- **重叠触发**:同一任务已有运行实例时,新触发记为 `skipped`,不会并发。
|
||
- **没有全局并发上限**:10 个任务同时到点会各自启动一个进程。
|
||
- **取消**:通过 `CancellationToken` + `TerminateJobObject`,5 秒内未退出则 `kill`。
|
||
- **超时**:与取消同一路径,状态记为 `timed_out`。
|
||
- **取消后立即重跑**:允许;旧进程终止与新进程启动可能短暂重叠。
|
||
- **错过的触发不补跑**:下次触发始终从"现在"开始计算。
|
||
- **下次触发时间**:调度器保存在内存中,`GET /api/v1/schedule` 暴露给前端。
|
||
- **重试**:字段已持久化但执行器尚未实现自动重试(见「已知限制」)。
|
||
|
||
---
|
||
|
||
## 日志与编码
|
||
|
||
文件布局:`logs/{task_id}/{run_id}.log`(UTF-8 写入)
|
||
|
||
- 每次运行 **5 MiB** 上限,超出后丢弃后续输出并写一条 `[system]` 说明。
|
||
- **单行 256 KiB** 上限:超出部分丢弃、继续读到行尾,避免内存无界增长。
|
||
- 保留期(默认 30 天)与总容量(默认 512 MiB)清理;**跳过正在写入的日志**。
|
||
- 读取按 64 KiB 分块且对齐 UTF-8 边界;`complete` 只在"运行结束且读到文件末尾"时为真。
|
||
- **合并推送**:子进程输出按行写入文件后,先在内存里按 **100 ms / 16 KiB** 窗口合并,
|
||
每个窗口只发一条 SSE `log` 事件(偏移仍严格连续);低输出时由后台任务按时推,
|
||
运行结束时在 `LogSink` 析构里兜底 flush,不会丢尾部。
|
||
- **虚拟滚动**:日志抽屉只渲染视口附近的行(固定行高 20 px),几万行时 DOM 节点数恒定;
|
||
增量先并进行缓冲、一帧最多重渲染一次。往上翻时停止跟随并显示「回到最新」。长行横向滚动、不折行。
|
||
- **输出编码**:默认 `auto` = 逐行先按 UTF-8 解码,失败回退到系统 ANSI 代码页(简体中文为 GBK)。
|
||
可用 `TASK_PILOT_LOG_ENCODING=gbk|utf-8|windows-1251` 强制。
|
||
- **脱敏**:环境变量/Header 名包含 `SECRET`、`TOKEN`、`PASSWORD`、`PASS`、`API_KEY`、
|
||
`PRIVATE_KEY`、`CREDENTIAL`、`AUTHORIZATION`、`AUTH` 时,其值在日志中被替换为 `[REDACTED]`。
|
||
|
||
---
|
||
|
||
## Python 与虚拟环境
|
||
|
||
编辑器和执行器共用同一份解析逻辑:
|
||
|
||
1. 手动指定的 `action.venv`
|
||
2. 从解释器路径推断(`...\.venv\Scripts\python.exe`)
|
||
3. 从工作目录向上查找 `.venv`、`venv`、`env`
|
||
|
||
虚拟环境必须同时存在 `pyvenv.cfg` 与 `Scripts\python.exe`,否则显式配置在保存时被拒绝。
|
||
|
||
**没有虚拟环境时**:
|
||
|
||
- 先看用户自己的环境:任务里指定的 `venv`、解释器路径、工作目录向上查找,命中就直接用,不重复创建。
|
||
- 都没有时,**新建任务时会用 uv 自动创建 `.venv`**:`pyproject.toml` 走 `uv sync`,
|
||
仅有 `requirements*.txt` 走 `uv venv .venv` + `uv pip install -r`,纯标准库脚本(没有依赖文件)建一个空环境。
|
||
保存按钮会显示「正在创建虚拟环境…」,环境问题当场暴露。手动路径是编辑器里的「用 uv 创建环境」按钮。
|
||
- 运行期只做校验、不再自动装依赖:没有 `.venv` 的 Python 任务会直接失败并提示先创建环境。
|
||
- 创建时会使用当前默认 PyPI 镜像:注入 `UV_DEFAULT_INDEX`(实际生效的是它)以及兼容用的 `UV_INDEX_URL` / `PIP_INDEX_URL`,不会绕过镜像设置。
|
||
- uv 只用平台认的两份:程序目录下的 `uv\uv.exe`(「程序目录 uv」,默认),或你指定的 `uv.exe`(「指定路径」)。
|
||
**不读系统 PATH**——PATH 上的 uv 版本会随启动方式变化,不适合当调度器的隐式依赖。
|
||
没有 uv 时在 uv 配置弹窗点「下载 uv」,从科大镜像
|
||
`mirrors.ustc.edu.cn/github-release/astral-sh/uv/LatestRelease` 下载当前架构的压缩包,
|
||
校验 sha256 后解压到 `uv\uv.exe` 并自动切换过去;本机已经装了 uv 的话,把它填进「指定路径」即可。
|
||
- 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`。
|
||
自定义解释器仍可通过 API 的 `action.interpreter` 字段传入。
|
||
|
||
命中虚拟环境后注入:
|
||
|
||
| 变量 | 行为 |
|
||
| --- | --- |
|
||
| `PATH` | 虚拟环境 `Scripts` 目录置前,保留用户 PATH,按大小写不敏感去重 |
|
||
| `VIRTUAL_ENV` / `VIRTUAL_ENV_PROMPT` | 指向虚拟环境根目录 |
|
||
| `PYTHONUNBUFFERED` | 强制 `1`(实时日志依赖) |
|
||
| `PYTHONIOENCODING` | 默认 `utf-8`,用户显式设置优先;避免 GBK 控制台下打印 emoji/生僻字符直接崩溃 |
|
||
| `PYTHONHOME` / `PYTHONPATH` | 虚拟环境模式下清理继承的污染值 |
|
||
|
||
编辑器会实时显示 `● 已关联虚拟环境: .venv\Scripts\python.exe`,并同步更新调用预览。
|
||
|
||
---
|
||
|
||
## Web 界面
|
||
|
||
- **顶部品牌栏**:品牌 + 右侧 Rust 引擎在线状态与版本;页面级导航已去掉,配置都收到任务调度页的卡片里。
|
||
- **三栏工作台**(任务调度页):左栏用户卡片 + 概览指标 + 调度摘要;中栏搜索筛选 + 按类型(脚本 / HTTP / 二进制)分组的任务卡片与分页;
|
||
右栏 uv(版本号 / 缓存目录 / Python 目录)、镜像源、托管虚拟环境摘要——点「配置 / 管理」打开对应**弹窗**。
|
||
- **任务卡片**:名称、任务标识、状态、Cron、下次触发,以及运行 / 日志 / 编辑 / 启停 / 删除。
|
||
- **编辑器**:先选任务类型(脚本 / HTTP / 二进制),脚本类型再选语言(Python / Shell / Node / Lua),
|
||
非 Python 语言会自动隐藏虚拟环境字段并调整脚本占位符;Cron 预览(实时计算未来 3/5/8/10 次)、
|
||
时区下拉、虚拟环境状态条、调用预览;探测到虚拟环境会自动回填「虚拟环境目录」,仍可手动修改;
|
||
缺少虚拟环境但项目有依赖文件时保存会自动用 uv 创建。解释器由语言自动选择,界面不再要求手填。
|
||
- **目录选择**:页面内目录浏览器(盘符 → 逐级进入 → 使用此目录),同时保留手动输入。
|
||
- **日志弹窗**:运行历史筛选、SSE 实时日志、取消运行、下载日志、Escape/焦点陷阱。
|
||
- **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` 生成,缺失时回退系统字体。
|
||
|
||
---
|
||
|
||
## 安全模型
|
||
|
||
| 边界 | 实现 |
|
||
| --- | --- |
|
||
| 监听地址 | 固定 `127.0.0.1`(`TASK_PILOT_PORT` 只改端口) |
|
||
| Origin | 带 `Origin` 的请求必须是 `http://127.0.0.1:*` 或 `http://localhost:*`,否则 403 |
|
||
| 管理令牌 | 非 GET/HEAD/OPTIONS 必须带 `X-TaskPilot-Token`;进程启动时随机生成,重启轮换 |
|
||
| 比较方式 | 常量时间比较,避免时序侧信道 |
|
||
| 请求体 | 上限 2 MiB |
|
||
| 命令执行 | 参数以 argv 传递,不经过 Shell 拼接 |
|
||
| 读取接口 | 只读接口(任务列表、日志、目录列举、统计)免令牌 |
|
||
| 前端令牌 | 客户端缓存令牌;写请求 403 时丢弃缓存并重试一次 |
|
||
|
||
> 这是**本地单用户工具**的威胁模型:本机其他进程仍可调用只读接口。不要把端口暴露到局域网。
|
||
|
||
---
|
||
|
||
## 配置项
|
||
|
||
| 变量 | 默认值 | 说明 |
|
||
| --- | --- | --- |
|
||
| `TASK_PILOT_PORT` | `3000` | 监听端口(地址固定回环) |
|
||
| `TASK_PILOT_DATABASE_URL` | `sqlite://data/task_pilot.db` | SQLite 路径;`sqlite::memory:` 用于测试 |
|
||
| `TASK_PILOT_LOG_DIR` | `logs` | 运行日志根目录 |
|
||
| `TASK_PILOT_STATIC_DIR` | `frontend/dist` | 生产静态资源目录(打包版由 `start.ps1` 设为 `web`) |
|
||
| `TASK_PILOT_LOG_RETENTION_DAYS` | `30` | 日志保留天数 |
|
||
| `TASK_PILOT_LOG_MAX_TOTAL_MB` | `512` | 日志总容量上限 |
|
||
| `TASK_PILOT_LOG_ENCODING` | `auto` | 子进程输出编码;`auto` 为 UTF-8→系统代码页回退 |
|
||
| `RUST_LOG` | `task_pilot_backend=debug,tower_http=debug` | 日志级别 |
|
||
|
||
数据库连接:SQLite 开启 `foreign_keys`、**WAL**、`busy_timeout=5s`、`synchronous=NORMAL`,最多 5 个连接。
|
||
|
||
---
|
||
|
||
## API 参考
|
||
|
||
### 系统
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
| --- | --- | --- |
|
||
| `GET` | `/api/v1/health` | 健康检查 |
|
||
| `GET` | `/api/v1/session` | 获取当前进程的管理令牌 |
|
||
| `GET` | `/api/v1/openapi.json` | 运行时 OpenAPI 文档 |
|
||
| `POST` | `/api/v1/cron/preview` | 计算未来若干次触发时间 |
|
||
| `POST` | `/api/v1/python/venv` | 探测工作目录会关联的虚拟环境、uv 可用性与依赖文件 |
|
||
| `POST` | `/api/v1/python/venv/create` | 用 uv 创建/同步项目 `.venv`(优先复用已有环境) |
|
||
| `GET` | `/api/v1/system/directories?path=` | 列盘符或子文件夹(目录浏览器) |
|
||
| `GET` | `/api/v1/stats` | 仪表盘计数(总数/启用/运行中/三类数量) |
|
||
| `GET` | `/api/v1/schedule` | 已启用任务的下次触发时间 |
|
||
|
||
### Python 镜像
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
| --- | --- | --- |
|
||
| `GET` | `/api/v1/python/mirrors` | 镜像列表(默认项优先) |
|
||
| `POST` | `/api/v1/python/mirrors` | 新增镜像 |
|
||
| `GET` | `/api/v1/python/mirrors/{id}` | 镜像详情 |
|
||
| `PUT` | `/api/v1/python/mirrors/{id}` | 更新名称 / 地址 / 信任主机 / 启用状态 |
|
||
| `DELETE` | `/api/v1/python/mirrors/{id}` | 删除镜像 |
|
||
| `POST` | `/api/v1/python/mirrors/{id}/default` | 设为默认(同一时间仅一个默认,并自动启用) |
|
||
|
||
### 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\` 并切换过去 |
|
||
|
||
### 托管虚拟环境
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
| --- | --- | --- |
|
||
| `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` 预装) |
|
||
| `GET` | `/api/v1/python/envs/{name}/packages` | 已安装的包(`uv pip list --format json`) |
|
||
| `DELETE` | `/api/v1/python/envs/{name}` | 删除整个环境目录 |
|
||
|
||
### 任务与运行
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
| --- | --- | --- |
|
||
| `GET` | `/api/v1/tasks` | 任务列表(`q`、`enabled`、`action_type`、`page`、`page_size`) |
|
||
| `POST` | `/api/v1/tasks` | 创建任务 |
|
||
| `GET` | `/api/v1/tasks/{id}` | 任务详情 |
|
||
| `PUT` | `/api/v1/tasks/{id}` | 完整更新 |
|
||
| `DELETE` | `/api/v1/tasks/{id}` | 删除(级联删除运行记录) |
|
||
| `POST` | `/api/v1/tasks/{id}/enable` | 启用并注册调度 |
|
||
| `POST` | `/api/v1/tasks/{id}/disable` | 停用并取消调度 |
|
||
| `POST` | `/api/v1/tasks/{id}/run` | 手动立即执行(`202`) |
|
||
| `GET` | `/api/v1/tasks/{id}/runs` | 运行历史(`status`、`trigger`、分页) |
|
||
| `GET` | `/api/v1/runs/{run_id}` | 单次运行详情 |
|
||
| `GET` | `/api/v1/runs/{run_id}/logs?offset=0` | 增量日志(`content`、`next_offset`、`complete`) |
|
||
| `GET` | `/api/v1/runs/{run_id}/logs/download` | 下载完整日志 |
|
||
| `POST` | `/api/v1/runs/{run_id}/cancel` | 取消运行 |
|
||
| `GET` | `/api/v1/events` | SSE:`run` / `log` / `lagged` / `ready` |
|
||
|
||
### 示例
|
||
|
||
创建任务:
|
||
|
||
```http
|
||
POST /api/v1/tasks
|
||
X-TaskPilot-Token: <session token>
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"id": "sync_data",
|
||
"name": "数据同步",
|
||
"cron": "0 0 2 * * *",
|
||
"timezone": "Asia/Shanghai",
|
||
"enabled": true,
|
||
"timeout_secs": 300,
|
||
"action": {
|
||
"type": "script",
|
||
"working_dir": "D:\\Projects\\sync",
|
||
"interpreter": "python.exe",
|
||
"script": "main.py",
|
||
"args": ["--mode", "full"],
|
||
"env": {}
|
||
}
|
||
}
|
||
```
|
||
|
||
统计数据:
|
||
|
||
```json
|
||
{ "total": 12, "enabled": 9, "running": 1, "script": 7, "http": 3, "process": 2 }
|
||
```
|
||
|
||
调度信息:
|
||
|
||
```json
|
||
[ { "task_id": "sync_data", "next_run_at": "2026-10-26T18:00:00Z" } ]
|
||
```
|
||
|
||
统一错误结构:
|
||
|
||
```json
|
||
{ "code": "VALIDATION_ERROR", "message": "Cron 表达式无效", "field": "cron", "details": {} }
|
||
```
|
||
|
||
常见错误码:`VALIDATION_ERROR`、`INVALID_JSON`、`INVALID_QUERY`、`TASK_NOT_FOUND`、
|
||
`RUN_NOT_FOUND`、`RUN_LOG_NOT_FOUND`、`RUN_NOT_CANCELLABLE`、`TASK_ID_CONFLICT`、
|
||
`DIRECTORY_PATH_INVALID`、`DIRECTORY_NOT_FOUND`、`DIRECTORY_UNREADABLE`、`FORBIDDEN`、`INTERNAL_ERROR`。
|
||
|
||
---
|
||
|
||
## 输入校验
|
||
|
||
保存任务时逐项强制(失败返回 `400 VALIDATION_ERROR` 并带 `field`):
|
||
|
||
| 字段 | 规则 |
|
||
| --- | --- |
|
||
| `id` | 1–64,`[A-Za-z0-9_-]`;重复返回 `409 TASK_ID_CONFLICT` |
|
||
| `name` | 去空格后 1–120 字符 |
|
||
| `cron` | 可解析且存在未来触发点 |
|
||
| `timezone` | 有效 IANA 名称 |
|
||
| `timeout_secs` | 1–86400 |
|
||
| `retry_policy` | `max_attempts ≤ 10`,`delay_secs ≤ 3600` |
|
||
| `action.working_dir` / `script` | 非空 |
|
||
| `action.language` | `python` / `shell` / `node` / `lua`;可省略,省略时按解释器推断 |
|
||
| `action.interpreter` | 可空;空时按语言默认(Python 为 `python.exe`,Shell 为 `cmd.exe`,Node 为 `node.exe`,Lua 为 `lua.exe`) |
|
||
| `action.venv` | 若填写,必须存在 `pyvenv.cfg` 与 `Scripts\python.exe` |
|
||
| `action.args` | ≤ 256 个,单个 ≤ 2048 字符 |
|
||
| `action.env` | ≤ 128 条;key ≤ 128、不含 `=`/`\0`;value ≤ 8192、不含 `\0` |
|
||
| `action.url` | 可解析、限 `http`/`https`、必须有主机名 |
|
||
| `action.headers` | 名称/值符合 HTTP 规范 |
|
||
| `action.body` | ≤ 1 MiB |
|
||
| `action.expected_status` | 每项 100–599 |
|
||
| 请求体 | 未知字段直接拒绝(`deny_unknown_fields`) |
|
||
|
||
查询与读取:`page ≥ 1`、`page_size` 夹到 1–100、日志 `offset` 不得超过文件长度。
|
||
|
||
**不在保存时校验**:工作目录是否存在、脚本文件是否存在、解释器是否存在(虚拟环境除外)。
|
||
这些在执行时才暴露,属于日志里的运行错误。
|
||
|
||
---
|
||
|
||
## 数据与文件布局
|
||
|
||
```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)。
|
||
|
||
---
|
||
|
||
## 架构与模块
|
||
|
||
```text
|
||
React (Vite) ──fetch──► Axum router ──security::api_guard(Origin + token)
|
||
│
|
||
├── tasks: routes → service(校验) → repository(SQL)
|
||
├── runs : routes → repository
|
||
├── Scheduler ──trigger──► TaskExecutor ──► Job Object
|
||
└── broadcast(512) ──► SSE /api/v1/events
|
||
```
|
||
|
||
| 模块 | 职责 |
|
||
| --- | --- |
|
||
| `api.rs` | 路由、OpenAPI 聚合、系统级接口(health/session/cron/stats/schedule) |
|
||
| `tasks/` | 任务模型、校验、SQL、CRUD 路由 |
|
||
| `runs/` | 运行记录、执行器、日志落盘、日志读取与 SSE |
|
||
| `scheduler.rs` | 每任务一个调度循环、下次触发时间、注册与取消 |
|
||
| `executor.rs` | 三类 Action 执行、并发闸门、超时/取消、日志脱敏与截断 |
|
||
| `process_job.rs` | Windows Job Object(`KILL_ON_JOB_CLOSE`) |
|
||
| `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 白名单、令牌校验、常量时间比较 |
|
||
| `database.rs` | 连接池与迁移(WAL/busy_timeout) |
|
||
|
||
前端:
|
||
|
||
| 模块 | 职责 |
|
||
| --- | --- |
|
||
| `@stash/ui`(同级 `../stash-ui`) | 设计令牌与通用组件:按钮、输入、表格、标签、弹窗、分页、Toast、排版 |
|
||
| `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` | 页面内目录浏览器 |
|
||
| `components/RunLogDrawer.tsx` | 运行历史、日志、取消、下载、SSE |
|
||
| `components/LogViewer.tsx` | 日志虚拟滚动、跟随底部/回到最新 |
|
||
| `lib/logLines.ts` | 增量日志的行缓冲(残行拼接、CRLF 归一) |
|
||
| `lib/logWindow.ts` | 可见行窗口计算 |
|
||
| `api/client.ts` | 类型化客户端、令牌缓存、403 重试 |
|
||
| `lib/taskForm.ts` | Zod 校验与表单 ↔ API 载荷映射 |
|
||
|
||
---
|
||
|
||
## 开发与测试
|
||
|
||
### 环境
|
||
|
||
- Windows 10/11
|
||
- Rust 1.95(`rust-toolchain.toml` 固定,MSVC)
|
||
- Node.js 24 / pnpm 11(`frontend/package.json` 的 `volta` 固定)
|
||
- Visual Studio C++ Build Tools(Rust MSVC 需要)
|
||
- 可选:`uv`(跑示例任务与自检脚本)
|
||
|
||
### 完整质量门
|
||
|
||
```powershell
|
||
.\scripts\check.ps1
|
||
```
|
||
|
||
依次执行:前端 lint → 类型检查 → Vitest → 生产构建 → `cargo fmt --check` →
|
||
`clippy -D warnings` → `cargo test` → Playwright(真实后端 + Vite + Chrome)。
|
||
|
||
脚本会把 `TMP`/`TEMP` 指向仓库内 `.tmp/`,让测试不依赖系统临时目录的权限策略,
|
||
结束时自动清理。
|
||
|
||
### 单独命令
|
||
|
||
```powershell
|
||
pnpm --dir frontend lint | typecheck | test | build
|
||
cargo fmt --manifest-path backend/Cargo.toml -- --check
|
||
cargo clippy --manifest-path backend/Cargo.toml --all-targets -- -D warnings
|
||
cargo test --manifest-path backend/Cargo.toml
|
||
pnpm --dir frontend test:e2e
|
||
```
|
||
|
||
### 契约与类型
|
||
|
||
```powershell
|
||
.\scripts\generate-api.ps1 # 更新 contracts/openapi.json 与 frontend/src/api/generated.ts
|
||
```
|
||
|
||
生成文件不要手工修改。
|
||
|
||
### 界面字体(可选)
|
||
|
||
字体二进制体积约 6.7 MB,**不纳入仓库**。首次克隆后如需与设计一致的中文界面:
|
||
|
||
```powershell
|
||
uv run --with fonttools --with brotli tools/build-fonts.py
|
||
```
|
||
|
||
脚本会从 `lxgw-wenkai-webfont` 的 CDN 下载分包、合并并裁剪到 GB2312 + 中英文标点,
|
||
输出到 `frontend/public/fonts/`(已被 `.gitignore` 忽略)。不生成也能正常运行,
|
||
只是正文回退到系统字体;`Outfit-Medium.ttf`(55 KB)仍随仓库提供。
|
||
|
||
### 测试构成
|
||
|
||
| 层 | 覆盖 |
|
||
| --- | --- |
|
||
| Rust 单元(73) | Cron、虚拟环境解析与探测、项目依赖文件扫描、uv 来源解析/校验/压缩包解压/`UV_*` 环境变量校验、托管环境名校验、PATH 注入去重、执行器(编码/截断/清理/句柄归属/日志合并推送)、安全、目录列举 |
|
||
| Rust 集成(48) | 任务 CRUD 与校验、运行生命周期、取消与超时、日志分块、调度恢复、统计与调度接口、重启对账、镜像 CRUD、uv 设置读写与下载鉴权、托管环境校验与 404、venv 探测/创建、无环境运行提示、Shell 脚本执行 |
|
||
| 前端 Vitest(37) | 三栏工作台渲染、保存 uv 来源与 `UV_*` 环境变量、镜像下载、Python 版本下载、托管环境创建与包列表、新建任务前先建虚拟环境、表单载荷与脚本语言、虚拟环境状态、uv 创建入口、目录浏览器、下次触发时间、日志行切分/窗口计算/虚拟滚动 |
|
||
| Playwright(8) | 真实任务执行与日志、编辑/停用/删除、脚本语言切换、目录切换单状态、窄屏、**无外部网络渲染**、1500 行日志的虚拟滚动、镜像增删与设默认 |
|
||
| Python 自检(14 项) | `tools/verify.py` 覆盖 API 全链路 |
|
||
|
||
> 前端 Vitest 里 6 个用例仍在按已删除的顶部导航点击(uv / 虚拟环境相关),
|
||
> 需要改成点右栏卡片并断言弹窗内容后才能全绿;改动停在 UI 调整阶段,尚未修。
|
||
|
||
---
|
||
|
||
## 故障排查
|
||
|
||
| 现象 | 处理 |
|
||
| --- | --- |
|
||
| `start.ps1` 无法运行 | `powershell -ExecutionPolicy Bypass -File .\release\TaskPilot\start.ps1` |
|
||
| 端口被占用 | `$env:TASK_PILOT_PORT = "3100"` 后重启 |
|
||
| 任务不执行 | 确认已启用、Cron 与下次触发时间正确;查看运行历史里的错误信息 |
|
||
| PowerShell 脚本任务报「不是数字签名」 | 执行策略拦未签名脚本:在参数里加 `-ExecutionPolicy Bypass`,或对脚本目录 `Unblock-File` |
|
||
| 日志出现 `����` | 程序未按 UTF-8 输出:设置 `TASK_PILOT_LOG_ENCODING=gbk`(或让程序输出 UTF-8) |
|
||
| 日志尾部缺失 | 单次运行超过 5 MiB 或单行超过 256 KiB,会截断并写 `[system]` 说明 |
|
||
| 运行一直显示"运行中" | 已由启动对账修复;若仍出现,检查进程是否被系统挂起 |
|
||
| 虚拟环境未生效 | 目录下需同时有 `pyvenv.cfg` 与 `Scripts\python.exe`;也可在表单里显式指定 |
|
||
| `cargo test` 报临时目录拒绝访问 | 用 `.\scripts\check.ps1`(会把 TEMP 指向 `.tmp/`)或手动设置 `TEMP` 到可写目录 |
|
||
|
||
---
|
||
|
||
## 已知限制
|
||
|
||
- 自动重试尚未实现:`retry_policy` 只校验与存储。
|
||
- 并发策略只有 `skip`;没有全局并发上限与队列。
|
||
- HTTP Action 只校验状态码,不支持响应体断言、认证方案或 Header 断言。
|
||
- 敏感值脱敏基于变量名关键词与值替换,不是凭据保险箱。
|
||
- Conda 环境不会自动激活,需要显式填写解释器路径。
|
||
- Python 任务缺少虚拟环境时,新建任务会先用 uv 创建(需要网络与可写的 uv 缓存目录);运行期不会再自动装依赖。已有环境的项目不会被自动升级。
|
||
- 托管虚拟环境只负责创建 / 删除 / 查看已装包,不会自动升级依赖;任务运行也不会改动它。
|
||
- 默认缓存与 Python 目录在程序目录下(`uv-cache` / `python`),删除发布目录等于丢掉缓存和解释器;想复用全局目录就把 `UV_CACHE_DIR`、`UV_PYTHON_INSTALL_DIR` 改成绝对路径。
|
||
- 「下载 uv」依赖 `mirrors.ustc.edu.cn` 可达;网络不通时请自己下载 uv 放到程序同级 `uv` 目录或指定路径。
|
||
- 本地单用户工具:读取接口免令牌,不支持远程部署与多用户。
|
||
- 未安装为 Windows 服务;关闭启动脚本后调度停止。
|
||
- 错过的调度不会补跑。
|
||
- 界面字体不入库(体积原因),需用 `tools/build-fonts.py` 生成;缺失时回退系统字体。
|
||
|
||
---
|
||
|
||
## 架构决策记录
|
||
|
||
- [ADR-0001:Cron 解析与任务调度](docs/decisions/0001-cron-engine.md)
|
||
- [ADR-0002:前后端开发与生产托管](docs/decisions/0002-static-hosting.md)
|
||
- [ADR-0003:Python 虚拟环境启动环境](docs/decisions/0003-python-environment.md)
|
||
- [ADR-0004:运行日志的输出编码](docs/decisions/0004-log-encoding.md)
|
||
- [ADR-0005:由后端提供系统文件夹选择](docs/decisions/0005-folder-picker.md)
|
||
- [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)
|
||
|
||
## 开发计划
|
||
|
||
完整架构、数据模型、阶段任务与验收标准见 [PLAN.md](PLAN.md)。六个阶段均已完成,当前处于维护与迭代阶段。
|