feat: 支持 uv 从 pyproject/requirements 创建虚拟环境
- 工作目录探测新增 pyproject.toml / requirements*.txt 扫描、uv 可用性与 can_create - 新增 POST /api/v1/python/venv/create:已有虚拟环境优先复用;pyproject 走 uv sync,requirements 走 uv venv + uv pip install,并使用默认 PyPI 镜像 - 编辑器在缺少虚拟环境且具备依赖文件时显示「用 uv 创建环境」;没有 uv 或没有依赖文件时明确报错 - 解释器字段改为可选,留空由服务端默认 python.exe 并使用虚拟环境;Python 任务缺少虚拟环境/依赖文件时运行直接失败,不再回退系统解释器 - 补后端探测/创建测试、前端入口测试与 README
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Text,
|
||||
Textarea,
|
||||
TextButton,
|
||||
toast,
|
||||
} from '@stash/ui'
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import {
|
||||
@@ -25,6 +26,7 @@ import {
|
||||
} from 'react-hook-form'
|
||||
import {
|
||||
ApiRequestError,
|
||||
createPythonVenv,
|
||||
previewCron,
|
||||
probePythonVenv,
|
||||
type Task,
|
||||
@@ -81,6 +83,7 @@ export function TaskEditorModal({
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
const [cronPreviewCount, setCronPreviewCount] = useState(5)
|
||||
const [pickerOpen, setPickerOpen] = useState(false)
|
||||
const queryClient = useQueryClient()
|
||||
// 记录「自动填入」的虚拟环境目录:用户手动改过就不再覆盖;
|
||||
// 工作目录变化时只有仍等于该值的字段会被清掉,交给新目录重新探测。
|
||||
const autoVenvRef = useRef('')
|
||||
@@ -155,7 +158,7 @@ export function TaskEditorModal({
|
||||
probePythonVenv(
|
||||
{
|
||||
working_dir: debouncedWorkingDir,
|
||||
interpreter: debouncedInterpreter,
|
||||
interpreter: debouncedInterpreter.trim() || 'python.exe',
|
||||
venv: debouncedVenv.trim() || null,
|
||||
},
|
||||
signal,
|
||||
@@ -165,6 +168,23 @@ export function TaskEditorModal({
|
||||
retry: false,
|
||||
staleTime: 15_000,
|
||||
})
|
||||
const createVenvMutation = useMutation({
|
||||
mutationFn: (workingDir: string) =>
|
||||
createPythonVenv({ working_dir: workingDir }),
|
||||
onSuccess: (probe, workingDir) => {
|
||||
toast.success('虚拟环境已创建')
|
||||
if (probe.venv_path) {
|
||||
const value = venvFieldValue(workingDir, probe.venv_path)
|
||||
autoVenvRef.current = value
|
||||
setValue('script.venv', value, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
void queryClient.invalidateQueries({ queryKey: ['python-venv'] })
|
||||
},
|
||||
onError: (error) => toast.error(error.message),
|
||||
})
|
||||
const linkedVenv = venvProbe.data?.status === 'linked' ? venvProbe.data : null
|
||||
const previewVirtualEnv =
|
||||
scriptPreview.venv.trim() || linkedVenv?.venv_path || ''
|
||||
@@ -497,15 +517,16 @@ export function TaskEditorModal({
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
label="解释器"
|
||||
label="解释器(可选)"
|
||||
htmlFor="task-interpreter"
|
||||
hint="Python 项目留空即可,自动使用虚拟环境里的 Python;Node / Shell 等再填"
|
||||
error={errors.script?.interpreter?.message}
|
||||
>
|
||||
<Input
|
||||
id="task-interpreter"
|
||||
{...register('script.interpreter')}
|
||||
className="mono-input"
|
||||
placeholder="python.exe"
|
||||
placeholder="留空自动使用虚拟环境 Python"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
@@ -515,6 +536,10 @@ export function TaskEditorModal({
|
||||
probe={venvProbe.data}
|
||||
pending={venvProbe.isPending}
|
||||
failed={venvProbe.isError}
|
||||
creating={createVenvMutation.isPending}
|
||||
onCreate={() =>
|
||||
createVenvMutation.mutate(scriptPreview.working_dir.trim())
|
||||
}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
@@ -865,6 +890,8 @@ interface VenvDetectorBarProps {
|
||||
probe: VenvProbeResponse | undefined
|
||||
pending: boolean
|
||||
failed: boolean
|
||||
creating: boolean
|
||||
onCreate: () => void
|
||||
}
|
||||
|
||||
function VenvDetectorBar({
|
||||
@@ -872,6 +899,8 @@ function VenvDetectorBar({
|
||||
probe,
|
||||
pending,
|
||||
failed,
|
||||
creating,
|
||||
onCreate,
|
||||
}: VenvDetectorBarProps) {
|
||||
if (!workingDir.trim()) {
|
||||
return (
|
||||
@@ -926,12 +955,31 @@ function VenvDetectorBar({
|
||||
}
|
||||
|
||||
if (probe.status === 'missing') {
|
||||
if (probe.can_create) {
|
||||
return (
|
||||
<div className="env-detector-bar" role="status">
|
||||
<div className="env-detector-message">
|
||||
<span className="env-status-badge warn">● {probe.message}</span>
|
||||
<span className="env-detector-hint">
|
||||
{probe.has_pyproject
|
||||
? 'pyproject.toml'
|
||||
: probe.requirements_files.join(' · ')}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
loading={creating}
|
||||
onClick={onCreate}
|
||||
>
|
||||
用 uv 创建环境
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="env-detector-bar" role="status">
|
||||
<span className="env-status-badge ok">
|
||||
● 使用系统解释器: <code>{probe.display_path}</code>
|
||||
</span>
|
||||
<span className="env-detector-hint">{probe.message}</span>
|
||||
<span className="env-status-badge danger">● {probe.message}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user