feat: auto-install Python deps from template pyproject.toml via uv

ExternalToolManager.discover() now accepts template root dir, detects
pyproject.toml and runs `uv sync` to create a venv. Tool invocation and
schema discovery inject the venv PATH/VIRTUAL_ENV so template tools can
import declared dependencies without manual installation.
This commit is contained in:
Fam Zheng
2026-03-09 15:22:35 +00:00
parent fa800b1601
commit c70fbc49f0
7 changed files with 197 additions and 32 deletions

View File

@@ -2,8 +2,11 @@
import { ref } from 'vue'
import type { PlanStepInfo } from '../types'
defineProps<{
const BASE = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/api`
const props = defineProps<{
steps: PlanStepInfo[]
projectId: string
}>()
const emit = defineEmits<{
@@ -11,6 +14,9 @@ const emit = defineEmits<{
}>()
const expandedSteps = ref<Set<number>>(new Set())
const expandedArtifact = ref<{ stepOrder: number; path: string } | null>(null)
const artifactContent = ref<string>('')
const artifactLoading = ref(false)
function toggleStep(order: number) {
if (expandedSteps.value.has(order)) {
@@ -33,6 +39,41 @@ function quoteStep(e: Event, step: PlanStepInfo) {
e.stopPropagation()
emit('quote', `[步骤${step.order}] ${step.description}`)
}
function artifactIcon(type: string) {
switch (type) {
case 'json': return '{ }'
case 'markdown': return 'MD'
default: return '📄'
}
}
async function toggleArtifact(e: Event, stepOrder: number, path: string) {
e.stopPropagation()
if (expandedArtifact.value?.stepOrder === stepOrder && expandedArtifact.value?.path === path) {
expandedArtifact.value = null
return
}
expandedArtifact.value = { stepOrder, path }
artifactLoading.value = true
artifactContent.value = ''
try {
const res = await fetch(`${BASE}/projects/${props.projectId}/files/${path}`)
if (res.ok) {
artifactContent.value = await res.text()
} else {
artifactContent.value = `Error: ${res.status} ${res.statusText}`
}
} catch (err) {
artifactContent.value = `Error: ${err}`
} finally {
artifactLoading.value = false
}
}
function isArtifactExpanded(stepOrder: number, path: string) {
return expandedArtifact.value?.stepOrder === stepOrder && expandedArtifact.value?.path === path
}
</script>
<template>
@@ -60,9 +101,24 @@ function quoteStep(e: Event, step: PlanStepInfo) {
{{ step.command }}
</div>
<div v-if="step.artifacts?.length" class="step-artifacts">
<span v-for="a in step.artifacts" :key="a.path" class="artifact-tag">
📄 {{ a.name }} <span class="artifact-type">{{ a.artifact_type }}</span>
</span>
<button
v-for="a in step.artifacts"
:key="a.path"
class="artifact-tag"
:class="{ active: isArtifactExpanded(step.order, a.path) }"
@click="toggleArtifact($event, step.order, a.path)"
:title="a.description || a.path"
>
<span class="artifact-icon">{{ artifactIcon(a.artifact_type) }}</span>
<span class="artifact-name">{{ a.name }}</span>
</button>
<div
v-if="expandedArtifact && step.artifacts.some(a => isArtifactExpanded(step.order, a.path))"
class="artifact-content"
>
<div v-if="artifactLoading" class="artifact-loading">加载中...</div>
<pre v-else>{{ artifactContent }}</pre>
</div>
</div>
</div>
<div v-if="!steps.length" class="empty-state">
@@ -209,12 +265,59 @@ function quoteStep(e: Event, step: PlanStepInfo) {
background: var(--bg-tertiary);
padding: 2px 8px;
border-radius: 4px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.15s;
}
.artifact-type {
font-size: 10px;
.artifact-tag:hover {
border-color: var(--accent);
color: var(--accent);
opacity: 0.8;
}
.artifact-tag.active {
border-color: var(--accent);
background: rgba(79, 195, 247, 0.12);
color: var(--accent);
}
.artifact-icon {
font-size: 10px;
font-weight: 600;
opacity: 0.7;
}
.artifact-name {
font-weight: 500;
}
.artifact-content {
width: 100%;
margin-top: 4px;
border-radius: 4px;
background: var(--bg-card);
border: 1px solid var(--border);
overflow: hidden;
}
.artifact-content pre {
margin: 0;
padding: 8px 12px;
font-size: 11px;
line-height: 1.5;
color: var(--text-primary);
overflow-x: auto;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.artifact-loading {
padding: 12px;
text-align: center;
color: var(--text-secondary);
font-size: 12px;
}
.empty-state {

View File

@@ -40,12 +40,12 @@ onMounted(async () => {
try {
const res = await api.getReport(props.workflowId)
html.value = await marked.parse(res.report)
await renderMermaid()
} catch (e: any) {
error.value = e.message
} finally {
loading.value = false
}
await renderMermaid()
})
</script>

View File

@@ -180,7 +180,7 @@ async function onSubmitComment(text: string) {
@submit="onSubmitRequirement"
/>
<div class="plan-exec-row">
<PlanSection :steps="planSteps" @quote="addQuote" />
<PlanSection :steps="planSteps" :projectId="projectId" @quote="addQuote" />
<div class="right-panel">
<div class="tab-bar">
<button class="tab-btn" :class="{ active: rightTab === 'log' }" @click="rightTab = 'log'">日志</button>