feat: 编辑器实时探测并显示已关联虚拟环境
fix: 按系统 ANSI 代码页解码子进程输出,修复中文日志乱码
This commit is contained in:
@@ -7,7 +7,13 @@ import {
|
||||
useFieldArray,
|
||||
useForm,
|
||||
} from 'react-hook-form'
|
||||
import { ApiRequestError, previewCron, type Task } from '../api/client'
|
||||
import {
|
||||
ApiRequestError,
|
||||
previewCron,
|
||||
probePythonVenv,
|
||||
type Task,
|
||||
type VenvProbeResponse,
|
||||
} from '../api/client'
|
||||
import {
|
||||
createEmptyTaskFormValues,
|
||||
mapServerFieldToFormField,
|
||||
@@ -57,6 +63,15 @@ export function TaskEditorModal({
|
||||
const scriptPreview = watch('script')
|
||||
const debouncedCron = useDebouncedValue(cronValue, 300)
|
||||
const debouncedTimezone = useDebouncedValue(timezoneValue, 300)
|
||||
const debouncedWorkingDir = useDebouncedValue(
|
||||
scriptPreview.working_dir ?? '',
|
||||
350,
|
||||
)
|
||||
const debouncedInterpreter = useDebouncedValue(
|
||||
scriptPreview.interpreter ?? '',
|
||||
350,
|
||||
)
|
||||
const debouncedVenv = useDebouncedValue(scriptPreview.venv ?? '', 350)
|
||||
const cronPreview = useQuery({
|
||||
queryKey: [
|
||||
'cron-preview',
|
||||
@@ -79,6 +94,31 @@ export function TaskEditorModal({
|
||||
Boolean(debouncedTimezone.trim()),
|
||||
retry: false,
|
||||
})
|
||||
const venvProbe = useQuery({
|
||||
queryKey: [
|
||||
'python-venv',
|
||||
debouncedWorkingDir,
|
||||
debouncedInterpreter,
|
||||
debouncedVenv,
|
||||
],
|
||||
queryFn: ({ signal }) =>
|
||||
probePythonVenv(
|
||||
{
|
||||
working_dir: debouncedWorkingDir,
|
||||
interpreter: debouncedInterpreter,
|
||||
venv: debouncedVenv.trim() || null,
|
||||
},
|
||||
signal,
|
||||
),
|
||||
enabled:
|
||||
open && actionType === 'script' && Boolean(debouncedWorkingDir.trim()),
|
||||
retry: false,
|
||||
staleTime: 15_000,
|
||||
})
|
||||
const linkedVenv = venvProbe.data?.status === 'linked' ? venvProbe.data : null
|
||||
const previewVirtualEnv =
|
||||
scriptPreview.venv.trim() || linkedVenv?.venv_path || ''
|
||||
const previewInterpreter = linkedVenv?.display_path ?? null
|
||||
const scriptArgs = useFieldArray({ control, name: 'script.args' })
|
||||
const scriptEnv = useFieldArray({ control, name: 'script.env' })
|
||||
const httpHeaders = useFieldArray({ control, name: 'http.headers' })
|
||||
@@ -306,17 +346,23 @@ export function TaskEditorModal({
|
||||
|
||||
{actionType === 'script' ? (
|
||||
<div className="dynamic-fields">
|
||||
<Field
|
||||
label="项目工作目录 (Working Directory)"
|
||||
error={errors.script?.working_dir?.message}
|
||||
>
|
||||
<input
|
||||
{...register('script.working_dir')}
|
||||
className="field-input mono"
|
||||
placeholder="D:\Projects\sync"
|
||||
/>
|
||||
</Field>
|
||||
<VenvDetectorBar
|
||||
workingDir={debouncedWorkingDir}
|
||||
probe={venvProbe.data}
|
||||
pending={venvProbe.isPending}
|
||||
failed={venvProbe.isError}
|
||||
/>
|
||||
<div className="form-grid">
|
||||
<Field
|
||||
label="工作目录"
|
||||
error={errors.script?.working_dir?.message}
|
||||
>
|
||||
<input
|
||||
{...register('script.working_dir')}
|
||||
className="field-input mono"
|
||||
placeholder="D:\Projects\sync"
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="解释器"
|
||||
error={errors.script?.interpreter?.message}
|
||||
@@ -327,17 +373,17 @@ export function TaskEditorModal({
|
||||
placeholder="python.exe"
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="虚拟环境目录 (可选)"
|
||||
hint="留空时自动探测;填写后优先使用该目录"
|
||||
>
|
||||
<input
|
||||
{...register('script.venv')}
|
||||
className="field-input mono"
|
||||
placeholder="例如:.venv"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Field
|
||||
label="虚拟环境目录 (可选)"
|
||||
hint="留空时自动从工作目录及上级目录探测 .venv、venv、env"
|
||||
>
|
||||
<input
|
||||
{...register('script.venv')}
|
||||
className="field-input mono"
|
||||
placeholder="例如:.venv"
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="脚本文件或相对路径"
|
||||
error={errors.script?.script?.message}
|
||||
@@ -412,9 +458,9 @@ export function TaskEditorModal({
|
||||
<span>Windows 原生环境</span>
|
||||
</div>
|
||||
<div className="cli-preview-body">
|
||||
{scriptPreview.venv.trim() ? (
|
||||
{previewVirtualEnv ? (
|
||||
<div className="cli-line-env">
|
||||
set VIRTUAL_ENV={scriptPreview.venv.trim()}
|
||||
set VIRTUAL_ENV={previewVirtualEnv}
|
||||
</div>
|
||||
) : null}
|
||||
{scriptPreview.env
|
||||
@@ -431,7 +477,7 @@ export function TaskEditorModal({
|
||||
</div>
|
||||
))}
|
||||
<div className="cli-line-cmd">
|
||||
{formatInterpreterPreview(scriptPreview)}{' '}
|
||||
{formatInterpreterPreview(scriptPreview, previewInterpreter)}{' '}
|
||||
{scriptPreview.script.trim() || 'main.py'}
|
||||
{formatArgumentPreview(scriptPreview.args)}
|
||||
</div>
|
||||
@@ -615,11 +661,95 @@ function formatArgumentPreview(argumentsList: KeyValueFormValue[]): string {
|
||||
|
||||
function formatInterpreterPreview(
|
||||
script: TaskFormValues['script'],
|
||||
linkedPythonPath?: string | null,
|
||||
): string {
|
||||
if (script.venv.trim()) {
|
||||
return `${script.venv.trim().replace(/[\\/]+$/, '')}\\Scripts\\python.exe`
|
||||
}
|
||||
return script.interpreter.trim() || 'python.exe'
|
||||
return linkedPythonPath ?? (script.interpreter.trim() || 'python.exe')
|
||||
}
|
||||
|
||||
interface VenvDetectorBarProps {
|
||||
workingDir: string
|
||||
probe: VenvProbeResponse | undefined
|
||||
pending: boolean
|
||||
failed: boolean
|
||||
}
|
||||
|
||||
function VenvDetectorBar({
|
||||
workingDir,
|
||||
probe,
|
||||
pending,
|
||||
failed,
|
||||
}: VenvDetectorBarProps) {
|
||||
if (!workingDir.trim()) {
|
||||
return (
|
||||
<div className="env-detector-bar" role="status">
|
||||
<span className="env-status-badge warn">
|
||||
● 请输入有效的工作目录以探测环境
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (pending) {
|
||||
return (
|
||||
<div className="env-detector-bar" role="status">
|
||||
<span className="env-status-badge pending">⏳ 正在探测解释器...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (failed || !probe) {
|
||||
return (
|
||||
<div className="env-detector-bar" role="status">
|
||||
<span className="env-status-badge warn">● 环境探测失败,可稍后重试</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (probe.status === 'linked') {
|
||||
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">
|
||||
Rust 自动前置 PATH 及 VIRTUAL_ENV
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (probe.status === 'not_applicable') {
|
||||
const label =
|
||||
probe.interpreter_kind === 'node' ? '识别为 Node.js' : '使用指定解释器'
|
||||
return (
|
||||
<div className="env-detector-bar" role="status">
|
||||
<span className="env-status-badge ok">
|
||||
● {label}: <code>{probe.display_path}</code>
|
||||
</span>
|
||||
<span className="env-detector-hint">{probe.message}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (probe.status === 'missing') {
|
||||
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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="env-detector-bar" role="status">
|
||||
<span className="env-status-badge warn">● {probe.message}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function isSensitiveName(name: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user