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:
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user