feat: 左侧导航改版与 Python 镜像管理

- 应用外壳换成 stash-ui Layout:左侧导航(任务调度 / Python 镜像)+ 页面标题区,窄屏折叠为顶部导航
- 新增 python_mirrors 表与 CRUD/设默认接口,内置清华、阿里云、中科大、PyPI 官方
- Python 脚本任务运行时注入默认镜像的 PIP_INDEX_URL / UV_INDEX_URL,任务自身环境变量优先
- 新增镜像管理页与表单,支持新增、编辑、启用/停用、设默认、删除
- 重新生成 OpenAPI 与 TS 类型;补齐后端镜像测试、前端单测与 e2e,修复窄屏页面被宽表格撑开
This commit is contained in:
2026-11-03 22:12:11 +08:00
parent d7a6e17531
commit 29fc579275
25 changed files with 2491 additions and 47 deletions
+48 -6
View File
@@ -1,5 +1,5 @@
use std::{
collections::HashMap,
collections::{BTreeMap, HashMap},
path::{Path, PathBuf},
process::Stdio,
sync::{
@@ -26,7 +26,7 @@ use tokio_util::sync::CancellationToken;
use crate::{
error::ApiResult,
process_job::ProcessJob,
python_env::prepare_script_environment,
python_env::{is_python_interpreter, prepare_script_environment},
runs::{
model::{RunStatus, RunTrigger, TaskRunResponse},
repository,
@@ -447,11 +447,48 @@ impl TaskExecutor {
};
let mut command = Command::new(&prepared.interpreter);
prepared.apply(&mut command);
let mut notes = Vec::new();
if let Some(venv) = &prepared.virtual_env {
notes.push(format!("使用 Python 虚拟环境: {}", venv.display()));
}
if is_python_interpreter(interpreter) {
match crate::python_mirrors::repository::default_enabled(&self.inner.pool).await
{
Ok(Some(mirror)) => {
if !env_has(env, "PIP_INDEX_URL") {
command.env("PIP_INDEX_URL", &mirror.index_url);
}
if !env_has(env, "UV_INDEX_URL") {
command.env("UV_INDEX_URL", &mirror.index_url);
}
if let Some(host) = mirror
.trusted_host
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
&& !env_has(env, "PIP_TRUSTED_HOST")
{
command.env("PIP_TRUSTED_HOST", host);
}
notes.push(format!(
"Python 包镜像: {} ({})",
mirror.name, mirror.index_url
));
}
Ok(None) => {}
Err(error) => {
tracing::warn!(
task_id = %task.id,
%error,
"failed to load default python mirror"
);
}
}
}
command.arg(script).args(args).current_dir(working_dir);
let startup_note = prepared
.virtual_env
.as_ref()
.map(|venv| format!("使用 Python 虚拟环境: {}", venv.display()));
let startup_note = (!notes.is_empty()).then(|| notes.join(";"));
self.run_command(task, run_id, cancellation, command, startup_note)
.await
}
@@ -1064,6 +1101,11 @@ async fn wait_after_termination(child: &mut Child) {
}
}
fn env_has(env: &BTreeMap<String, String>, key: &str) -> bool {
env.keys()
.any(|candidate| candidate.eq_ignore_ascii_case(key))
}
fn sensitive_values(action: &TaskAction) -> Vec<String> {
match action {
TaskAction::Script { env, .. } | TaskAction::Process { env, .. } => env