feat: add education-training and platform-engineering team templates

- Add 2 new team templates: 教育培训团队 (4 roles) and 天工平台工程团队 (5 roles)
- Fix orchestrator to support multi-template workflow types (remove hardcoded PM planner)
- Add _resolve_planner_role() to auto-detect planner based on team.config.workflow
- Add frontend buttons and API clients for new templates
- Merge 14 preset roles from 3 template families in get_preset_roles()
- Add creation guide and platform engineering usage docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
renjianbo
2026-06-17 00:09:54 +08:00
parent 7f4aeb021b
commit f612248f9e
8 changed files with 4046 additions and 0 deletions

146
frontend/src/api/teams.ts Normal file
View File

@@ -0,0 +1,146 @@
import api from './index'
export interface TeamMember {
id: string
team_id: string
agent_id: string
role: string
position: number
is_lead: boolean
created_at: string
}
export interface Team {
id: string
name: string
description: string
workspace_id: string | null
user_id: string
is_template: boolean
config: any
status: string
created_at: string
updated_at: string
members?: TeamMember[]
}
export interface PresetRole {
label: string
icon: string
description: string
}
export interface PhaseResult {
phase: number
name: string
role: string
agent_name: string | null
output: string
success: boolean
iterations?: number
tool_calls?: number
error?: string | null
}
export interface ExecuteResult {
team_id: string
team_name: string
project_description: string
started_at: string
completed_at?: string
plan: any
phases: PhaseResult[]
qa_review: any
final_deliverable: string
success: boolean
error?: string | null
}
/** 获取预置角色定义 */
export function getPresetRoles() {
return api.get('/api/v1/teams/preset-roles')
}
/** 列出团队 */
export function listTeams(params?: { workspace_id?: string }) {
return api.get('/api/v1/teams', { params })
}
/** 获取团队详情 */
export function getTeam(teamId: string) {
return api.get(`/api/v1/teams/${teamId}`)
}
/** 创建团队 */
export function createTeam(data: {
name: string
description?: string
workspace_id?: string
config?: any
}) {
return api.post('/api/v1/teams', data)
}
/** 更新团队 */
export function updateTeam(teamId: string, data: {
name?: string
description?: string
config?: any
status?: string
}) {
return api.put(`/api/v1/teams/${teamId}`, data)
}
/** 删除团队 */
export function deleteTeam(teamId: string) {
return api.delete(`/api/v1/teams/${teamId}`)
}
/** 一键创建软件公司虚拟团队模板 */
export function createSoftwareCompanyTemplate(workspaceId?: string) {
return api.post('/api/v1/teams/template/software-company', null, {
params: workspaceId ? { workspace_id: workspaceId } : {},
})
}
/** 一键创建教育培训团队模板 */
export function createEducationTrainingTemplate(workspaceId?: string) {
return api.post('/api/v1/teams/template/education-training', null, {
params: workspaceId ? { workspace_id: workspaceId } : {},
})
}
/** 一键创建天工平台工程团队模板 */
export function createPlatformEngineeringTemplate(workspaceId?: string) {
return api.post('/api/v1/teams/template/platform-engineering', null, {
params: workspaceId ? { workspace_id: workspaceId } : {},
})
}
/** 添加团队成员 */
export function addMember(teamId: string, data: {
agent_id: string
role: string
position?: number
is_lead?: boolean
}) {
return api.post(`/api/v1/teams/${teamId}/members`, data)
}
/** 移除团队成员 */
export function removeMember(teamId: string, memberId: string) {
return api.delete(`/api/v1/teams/${teamId}/members/${memberId}`)
}
/** 列出团队成员 */
export function listMembers(teamId: string) {
return api.get(`/api/v1/teams/${teamId}/members`)
}
/** 执行团队项目 */
export function executeProject(teamId: string, projectDescription: string, autoApproveFiles = true) {
return api.post(`/api/v1/teams/${teamId}/execute`, {
project_description: projectDescription,
auto_approve_files: autoApproveFiles,
}, { timeout: 600000 }) // 10 min timeout for long executions
}

View File

@@ -0,0 +1,970 @@
<template>
<MainLayout>
<div class="team-builder">
<!-- 顶部工具栏 -->
<div class="toolbar">
<div class="toolbar-left">
<el-input
v-model="teamName"
placeholder="团队名称"
style="width: 260px"
@change="onTeamNameChange"
/>
<el-button type="primary" @click="handleSaveTeam" :loading="saving">
<el-icon><Check /></el-icon> 保存团队
</el-button>
<el-button type="success" @click="handleCreateTemplate" :loading="creatingTemplate">
<el-icon><MagicStick /></el-icon> 软件公司模板
</el-button>
<el-button type="warning" @click="handleCreateEducationTemplate" :loading="creatingEduTemplate">
<el-icon><MagicStick /></el-icon> 教育培训模板
</el-button>
<el-button type="danger" @click="handleCreatePlatformTemplate" :loading="creatingPlatformTemplate">
<el-icon><MagicStick /></el-icon> 天工平台工程团队
</el-button>
</div>
<div class="toolbar-right">
<el-select v-model="selectedTeamId" placeholder="加载已有团队" clearable style="width: 220px" @change="handleLoadTeam">
<el-option v-for="t in teams" :key="t.id" :label="t.name" :value="t.id" />
</el-select>
<el-button @click="loadTeams" :loading="loadingTeams">
<el-icon><Refresh /></el-icon>
</el-button>
</div>
</div>
<!-- 主体区域 -->
<div class="main-area">
<!-- 左侧可用 Agent 列表 -->
<div class="left-panel">
<div class="panel-title">可用 Agent</div>
<el-input v-model="agentSearch" placeholder="搜索Agent..." size="small" style="margin-bottom: 8px" clearable />
<div class="agent-list">
<div
v-for="agent in filteredAgents"
:key="agent.id"
class="agent-item"
:class="{ active: selectedAgent?.id === agent.id }"
draggable="true"
@dragstart="onDragStart(agent)"
@click="selectedAgent = agent"
>
<span class="agent-name">{{ agent.name }}</span>
<el-tag size="small" type="info">{{ agent.agent_type || 'specialist' }}</el-tag>
</div>
<el-empty v-if="filteredAgents.length === 0" description="暂无可用Agent" :image-size="48" />
</div>
</div>
<!-- 中间角色槽位 -->
<div class="center-panel">
<div class="panel-title">团队角色</div>
<div class="role-slots">
<div
v-for="role in roles"
:key="role.key"
class="role-card"
:class="{
filled: roleSlots[role.key],
'drag-over': dragOverRole === role.key,
}"
@dragover.prevent="dragOverRole = role.key"
@dragleave="dragOverRole = null"
@drop="onDrop(role.key)"
>
<div class="role-header">
<span class="role-icon">{{ role.icon }}</span>
<span class="role-label">{{ role.label }}</span>
<el-tag v-if="roleSlots[role.key]?.is_lead" size="small" type="danger">Leader</el-tag>
</div>
<div class="role-desc">{{ role.description }}</div>
<div v-if="roleSlots[role.key]" class="role-assigned">
<el-tag type="success" closable @close="removeFromSlot(role.key)">
{{ roleSlots[role.key].name }}
</el-tag>
</div>
<div v-else class="role-empty">
<el-icon><Plus /></el-icon>
<span>拖拽 Agent 到此处</span>
</div>
</div>
</div>
</div>
<!-- 右侧Agent 详情 -->
<div class="right-panel" v-if="selectedAgent">
<div class="panel-title">Agent 配置</div>
<el-descriptions :column="1" size="small" border>
<el-descriptions-item label="名称">{{ selectedAgent.name }}</el-descriptions-item>
<el-descriptions-item label="类型">{{ selectedAgent.agent_type }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ selectedAgent.status }}</el-descriptions-item>
<el-descriptions-item label="描述">{{ selectedAgent.description || '无' }}</el-descriptions-item>
</el-descriptions>
<div style="margin-top: 12px">
<el-button size="small" @click="router.push(`/agents/${selectedAgent.id}/config`)">
编辑 Agent
</el-button>
</div>
</div>
</div>
<!-- 底部项目输入 + 执行 -->
<div class="bottom-area">
<div class="project-input">
<el-input
v-model="projectDescription"
type="textarea"
:rows="3"
placeholder="输入项目描述,例如:做一个支持用户注册登录的个人博客系统,包含文章发布、评论功能..."
/>
<el-switch
v-model="autoApproveFiles"
active-text="自动写入文件"
style="margin-left: 12px"
/>
<el-button
type="primary"
size="large"
@click="handleExecute('sync')"
:loading="executing"
:disabled="!projectDescription || !hasAnyRole"
style="margin-left: 12px"
>
<el-icon><VideoPlay /></el-icon> 执行项目
</el-button>
<el-button
size="large"
@click="handleExecute('stream')"
:loading="executingStream"
:disabled="!projectDescription || !hasAnyRole"
style="margin-left: 8px"
>
<el-icon><Connection /></el-icon> 流式执行
</el-button>
</div>
<!-- 执行结果 -->
<div v-if="executing || executingStream || executeResult || streamEvents.length" class="result-area">
<div class="result-header">
<h3>执行结果</h3>
<el-button v-if="executeResult || streamEvents.length" size="small" @click="executeResult = null; streamEvents = []; streamDeliverable = ''; streamProjectPath = ''; streamProjectFiles = []">清空</el-button>
</div>
<!-- 流式事件 -->
<div v-if="streamEvents.length" class="stream-events">
<div v-for="(evt, i) in streamEvents" :key="i" class="stream-event" :class="'event-' + evt.type">
<template v-if="evt.type === 'plan_start'">
<el-alert type="info" :closable="false">
PM 开始规划 团队: {{ evt.team_name }} ({{ evt.member_count }} 名成员)
</el-alert>
</template>
<template v-else-if="evt.type === 'plan_done'">
<el-alert :type="evt.success ? 'success' : 'error'" :closable="false">
PM 规划完成 {{ evt.plan?.phases?.length || 0 }} 个阶段
</el-alert>
</template>
<template v-else-if="evt.type === 'phase_start'">
<div class="phase-start">
<el-icon :size="18"><Loading /></el-icon>
阶段 {{ evt.phase }}: {{ evt.name }} ({{ evt.role }}) Agent: {{ evt.agent }}
</div>
</template>
<template v-else-if="evt.type === 'phase_done'">
<el-collapse>
<el-collapse-item>
<template #title>
<div style="display:flex;align-items:center;gap:8px">
<el-tag :type="evt.success ? 'success' : 'danger'" size="small">阶段{{ evt.phase }}</el-tag>
<span>{{ evt.name }}</span>
<span style="color:#909399;font-size:12px">({{ evt.role }})</span>
<span v-if="evt.iterations" style="color:#909399;font-size:12px">{{ evt.iterations }}次迭代</span>
</div>
</template>
<div class="output-content" v-html="renderMarkdown(evt.output || '')" />
</el-collapse-item>
</el-collapse>
</template>
<template v-else-if="evt.type === 'qa_done'">
<el-collapse>
<el-collapse-item>
<template #title>
<el-tag :type="evt.success ? 'success' : 'info'" size="small">QA审查</el-tag>
</template>
<div class="output-content" v-html="renderMarkdown(evt.output || '')" />
</el-collapse-item>
</el-collapse>
</template>
<template v-else-if="evt.type === 'final'">
<el-alert type="success" :closable="false">
项目交付完成 {{ evt.phase_count }} 个阶段
</el-alert>
</template>
<template v-else-if="evt.type === 'error'">
<el-alert type="error" :closable="false">{{ evt.content }}</el-alert>
</template>
</div>
</div>
<!-- 流式完成后显示最终交付物 -->
<div v-if="streamDeliverable" style="margin-top:12px">
<el-divider />
<h4 style="margin:0 0 8px 0">最终交付物</h4>
<div class="output-content" v-html="renderMarkdown(streamDeliverable)" />
</div>
<!-- 流式项目文件路径 -->
<div v-if="streamProjectPath" style="margin-top:12px">
<el-divider />
<h4 style="margin:0 0 8px 0">项目文件</h4>
<div style="margin-bottom:8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<span style="font-weight:600;white-space:nowrap">保存路径:</span>
<el-input :model-value="streamProjectPath" readonly size="small" style="flex:1;min-width:300px" />
<el-button size="small" @click="copyPath(streamProjectPath)">复制</el-button>
</div>
<div v-if="streamProjectFiles.length">
<span style="font-weight:600;font-size:13px">产出文件 ({{ streamProjectFiles.length }}):</span>
<ul style="margin-top:4px;padding-left:20px">
<li v-for="f in streamProjectFiles" :key="f" style="font-size:13px;color:#409eff;margin-bottom:4px;word-break:break-all">{{ f }}</li>
</ul>
</div>
<el-empty v-else description="暂无产出文件" :image-size="48" />
</div>
<!-- 同步结果 -->
<div v-if="executeResult" class="sync-result">
<el-tabs>
<el-tab-pane label="项目计划">
<pre class="plan-json">{{ JSON.stringify(executeResult.plan, null, 2) }}</pre>
</el-tab-pane>
<el-tab-pane v-for="p in executeResult.phases" :key="p.phase" :label="'阶段' + p.phase + ': ' + p.name">
<div class="phase-output">
<el-tag :type="p.success ? 'success' : 'danger'" style="margin-bottom: 8px">
{{ p.role }} {{ p.agent_name }}
</el-tag>
<div class="output-content" v-html="renderMarkdown(p.output)" />
</div>
</el-tab-pane>
<el-tab-pane v-if="executeResult.qa_review" label="QA审查">
<pre class="plan-json">{{ JSON.stringify(executeResult.qa_review, null, 2) }}</pre>
</el-tab-pane>
<el-tab-pane label="最终交付物">
<div class="output-content" v-html="renderMarkdown(executeResult.final_deliverable)" />
</el-tab-pane>
<el-tab-pane v-if="executeResult.project_path" label="项目文件">
<div style="margin-bottom:12px">
<span style="font-weight:600;margin-right:8px">保存路径:</span>
<el-input
:model-value="executeResult.project_path"
readonly
size="small"
style="width:500px;max-width:100%"
>
<template #append>
<el-button @click="copyPath(executeResult.project_path)">复制</el-button>
</template>
</el-input>
</div>
<div v-if="executeResult.files?.length">
<span style="font-weight:600">产出文件 ({{ executeResult.files.length }}):</span>
<ul style="margin-top:8px;padding-left:20px">
<li v-for="f in executeResult.files" :key="f" style="font-size:13px;color:#409eff;margin-bottom:4px;word-break:break-all">{{ f }}</li>
</ul>
</div>
<el-empty v-else description="暂无产出文件" :image-size="48" />
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</div>
</MainLayout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import MainLayout from '@/components/MainLayout.vue'
import {
Check, MagicStick, Refresh, Plus, VideoPlay, Connection, Loading,
} from '@element-plus/icons-vue'
import {
listTeams, getTeam, createTeam, updateTeam, createSoftwareCompanyTemplate,
createEducationTrainingTemplate, createPlatformEngineeringTemplate, addMember, removeMember, executeProject, getPresetRoles,
} from '@/api/teams'
import api from '@/api'
import { marked } from 'marked'
const router = useRouter()
interface Agent {
id: string
name: string
description?: string
agent_type?: string
status?: string
workflow_config?: any
}
interface RoleSlot {
key: string
label: string
icon: string
description: string
}
// 团队
const teamName = ref('我的软件团队')
const selectedTeamId = ref<string | null>(null)
const currentTeamId = ref<string | null>(null)
const teams = ref<any[]>([])
const saving = ref(false)
const creatingTemplate = ref(false)
const creatingEduTemplate = ref(false)
const creatingPlatformTemplate = ref(false)
const loadingTeams = ref(false)
// Agent
const agents = ref<Agent[]>([])
const agentSearch = ref('')
const selectedAgent = ref<Agent | null>(null)
// 角色槽位
const roles = ref<RoleSlot[]>([])
const roleSlots = ref<Record<string, any>>({})
// 拖拽
const dragOverRole = ref<string | null>(null)
let draggingAgent: Agent | null = null
// 项目执行
const projectDescription = ref('')
const executing = ref(false)
const executingStream = ref(false)
const executeResult = ref<any>(null)
const streamEvents = ref<any[]>([])
const streamDeliverable = ref<string>('')
const streamProjectPath = ref<string>('')
const streamProjectFiles = ref<string[]>([])
const autoApproveFiles = ref(true)
const hasAnyRole = computed(() => Object.values(roleSlots.value).some(v => !!v))
// 过滤 Agent
const filteredAgents = computed(() => {
if (!agentSearch.value) return agents.value
const kw = agentSearch.value.toLowerCase()
return agents.value.filter(a => a.name.toLowerCase().includes(kw))
})
// 加载预置角色
async function loadRoles() {
try {
const res = await getPresetRoles()
const roleMap = res.data?.roles || {}
roles.value = Object.entries(roleMap).map(([key, info]: [string, any]) => ({
key,
label: info.label,
icon: info.icon,
description: info.description,
}))
} catch (e) {
// 使用默认角色
roles.value = [
{ key: 'pm', label: '项目经理', icon: '📋', description: '分析需求、分解任务、制定计划、协调团队' },
{ key: 'designer', label: 'UI/UX设计师', icon: '🎨', description: '设计用户流程、线框图、组件层级、视觉规范' },
{ key: 'developer', label: '开发工程师', icon: '💻', description: '编写后端API、前端组件、业务逻辑、数据库设计' },
{ key: 'qa', label: '测试工程师', icon: '🔍', description: '审查交付物、发现缺陷、验证验收标准、质量把关' },
{ key: 'devops', label: 'DevOps工程师', icon: '🚀', description: '部署配置、CI/CD、环境管理、健康检查' },
]
}
}
// 加载 Agent 列表
async function loadAgents() {
try {
const res = await api.get('/api/v1/agents', { params: { page_size: 200 } })
agents.value = (Array.isArray(res.data) ? res.data : (res.data?.data || res.data?.items || [])) as Agent[]
} catch (e) {
console.error('加载 Agent 列表失败:', e)
}
}
// 加载团队列表
async function loadTeams() {
loadingTeams.value = true
try {
const res = await listTeams()
teams.value = (res.data?.data || []) as any[]
} catch (e) {
console.error('加载团队列表失败:', e)
} finally {
loadingTeams.value = false
}
}
// 加载团队详情
async function handleLoadTeam(teamId: string | null) {
if (!teamId) {
resetSlots()
currentTeamId.value = null
return
}
try {
const res = await getTeam(teamId)
const team = res.data?.data
if (team) {
teamName.value = team.name
currentTeamId.value = team.id
const slots: Record<string, any> = {}
if (team.members) {
for (const m of team.members) {
// 找到对应的 Agent
const agent = agents.value.find(a => a.id === m.agent_id)
if (agent) {
slots[m.role] = { ...agent, is_lead: m.is_lead, member_id: m.id }
}
}
}
roleSlots.value = slots
ElMessage.success(`已加载团队: ${team.name}`)
}
} catch (e) {
console.error('加载团队失败:', e)
}
}
// 保存团队
async function handleSaveTeam() {
saving.value = true
try {
const payload: any = { name: teamName.value }
if (currentTeamId.value) {
await updateTeam(currentTeamId.value, payload)
} else {
const res = await createTeam(payload)
currentTeamId.value = (res.data as any)?.data?.id
}
// 同步成员
if (currentTeamId.value) {
for (const [role, agent] of Object.entries(roleSlots.value)) {
if (agent) {
await addMember(currentTeamId.value, {
agent_id: (agent as any).id,
role,
position: roles.value.findIndex(r => r.key === role),
is_lead: (agent as any).is_lead || false,
})
}
}
}
ElMessage.success('团队已保存')
await loadTeams()
} catch (e: any) {
ElMessage.error(e?.response?.data?.detail || '保存失败')
} finally {
saving.value = false
}
}
// 一键创建软件公司模板
async function handleCreateTemplate() {
creatingTemplate.value = true
try {
const res = await createSoftwareCompanyTemplate()
const team = (res.data as any)?.data
if (team) {
currentTeamId.value = team.id
teamName.value = team.name
// 重新加载 agents因为创建了新 agent
await loadAgents()
// 映射成员到槽位
const slots: Record<string, any> = {}
if (team.members) {
for (const m of team.members) {
const agent = agents.value.find(a => a.id === m.agent_id)
if (agent) {
slots[m.role] = { ...agent, is_lead: m.is_lead, member_id: m.id }
}
}
}
roleSlots.value = slots
ElMessage.success('软件公司虚拟团队创建成功!')
}
await loadTeams()
} catch (e: any) {
ElMessage.error(e?.response?.data?.detail || '创建模板失败')
} finally {
creatingTemplate.value = false
}
}
// 一键创建教育培训模板
async function handleCreateEducationTemplate() {
creatingEduTemplate.value = true
try {
const res = await createEducationTrainingTemplate()
const team = (res.data as any)?.data
if (team) {
currentTeamId.value = team.id
teamName.value = team.name
await loadAgents()
const slots: Record<string, any> = {}
if (team.members) {
for (const m of team.members) {
const agent = agents.value.find(a => a.id === m.agent_id)
if (agent) {
slots[m.role] = { ...agent, is_lead: m.is_lead, member_id: m.id }
}
}
}
roleSlots.value = slots
ElMessage.success('教育培训团队创建成功!')
}
await loadTeams()
} catch (e: any) {
ElMessage.error(e?.response?.data?.detail || '创建模板失败')
} finally {
creatingEduTemplate.value = false
}
}
// 一键创建天工平台工程团队模板
async function handleCreatePlatformTemplate() {
creatingPlatformTemplate.value = true
try {
const res = await createPlatformEngineeringTemplate()
const team = (res.data as any)?.data
if (team) {
currentTeamId.value = team.id
teamName.value = team.name
await loadAgents()
const slots: Record<string, any> = {}
if (team.members) {
for (const m of team.members) {
const agent = agents.value.find(a => a.id === m.agent_id)
if (agent) {
slots[m.role] = { ...agent, is_lead: m.is_lead, member_id: m.id }
}
}
}
roleSlots.value = slots
ElMessage.success('天工平台工程团队创建成功!')
}
await loadTeams()
} catch (e: any) {
ElMessage.error(e?.response?.data?.detail || '创建模板失败')
} finally {
creatingPlatformTemplate.value = false
}
}
// 拖拽
function onDragStart(agent: Agent) {
draggingAgent = agent
}
function onDrop(role: string) {
if (draggingAgent) {
roleSlots.value[role] = draggingAgent
dragOverRole.value = null
draggingAgent = null
}
}
function removeFromSlot(role: string) {
delete roleSlots.value[role]
}
function resetSlots() {
roleSlots.value = {}
}
// 执行项目
async function handleExecute(mode: 'sync' | 'stream') {
if (!currentTeamId.value) {
ElMessage.warning('请先保存团队')
return
}
if (!projectDescription.value) {
ElMessage.warning('请输入项目描述')
return
}
if (mode === 'sync') {
executing.value = true
executeResult.value = null
streamDeliverable.value = ''
streamProjectPath.value = ''
streamProjectFiles.value = []
try {
const res = await executeProject(currentTeamId.value, projectDescription.value, autoApproveFiles.value)
executeResult.value = (res.data as any)?.data
} catch (e: any) {
ElMessage.error(e?.response?.data?.detail || '执行失败')
} finally {
executing.value = false
}
} else {
executingStream.value = true
streamEvents.value = []
streamDeliverable.value = ''
streamProjectPath.value = ''
streamProjectFiles.value = []
executeResult.value = null
const token = localStorage.getItem('token') || ''
const baseUrl = (api.defaults.baseURL || '').replace(/\/+$/, '')
const url = `${baseUrl}/api/v1/teams/${currentTeamId.value}/execute/stream`
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ project_description: projectDescription.value, auto_approve_files: autoApproveFiles.value }),
})
const reader = response.body?.getReader()
if (!reader) throw new Error('无法读取流')
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const evt = JSON.parse(line.slice(6))
streamEvents.value.push(evt)
if (evt.type === 'final') {
if (evt.deliverable) streamDeliverable.value = evt.deliverable
if (evt.project_path) streamProjectPath.value = evt.project_path
if (evt.files) streamProjectFiles.value = evt.files
}
if (evt.type === 'plan_start' && evt.project_path) {
streamProjectPath.value = evt.project_path
}
} catch { /* ignore parse errors */ }
}
}
}
} catch (e: any) {
streamEvents.value.push({ type: 'error', content: e.message })
} finally {
executingStream.value = false
}
}
}
// Markdown 渲染
function renderMarkdown(text: string): string {
if (!text) return ''
try {
return marked.parse(text) as string
} catch {
return text.replace(/\n/g, '<br>')
}
}
function onTeamNameChange() {
// 仅更新本地状态
}
async function copyPath(path: string) {
try {
await navigator.clipboard.writeText(path)
ElMessage.success('路径已复制到剪贴板')
} catch {
ElMessage.warning('复制失败,请手动复制')
}
}
onMounted(() => {
loadRoles()
loadAgents()
loadTeams()
})
</script>
<style scoped>
.team-builder {
display: flex;
flex-direction: column;
height: 100%;
gap: 12px;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #f5f7fa;
border-radius: 8px;
flex-wrap: wrap;
gap: 8px;
}
.toolbar-left {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.main-area {
display: flex;
flex: 1;
gap: 12px;
min-height: 0;
}
.left-panel {
width: 240px;
min-width: 200px;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #ebeef5;
}
.agent-list {
flex: 1;
overflow-y: auto;
min-height: 0;
}
.agent-item {
padding: 8px 10px;
margin-bottom: 4px;
border-radius: 6px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid transparent;
transition: all 0.2s;
}
.agent-item:hover {
background: #ecf5ff;
border-color: #c6e2ff;
}
.agent-item.active {
background: #d9ecff;
border-color: #409eff;
}
.agent-name {
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
margin-right: 8px;
}
.center-panel {
flex: 1;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
}
.role-slots {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.role-card {
border: 2px dashed #dcdfe6;
border-radius: 8px;
padding: 16px;
min-height: 140px;
transition: all 0.2s;
}
.role-card.filled {
border-color: #67c23a;
border-style: solid;
background: #f0f9eb;
}
.role-card.drag-over {
border-color: #409eff;
background: #ecf5ff;
transform: scale(1.02);
}
.role-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.role-icon {
font-size: 24px;
}
.role-label {
font-weight: 600;
font-size: 14px;
flex: 1;
}
.role-desc {
font-size: 12px;
color: #909399;
margin-bottom: 12px;
line-height: 1.5;
}
.role-assigned {
margin-top: auto;
}
.role-empty {
display: flex;
flex-direction: column;
align-items: center;
color: #c0c4cc;
font-size: 12px;
gap: 4px;
margin-top: auto;
}
.right-panel {
width: 260px;
min-width: 220px;
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
overflow-y: auto;
}
.bottom-area {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
}
.project-input {
display: flex;
align-items: flex-start;
}
.project-input .el-textarea {
flex: 1;
}
.result-area {
margin-top: 12px;
border-top: 1px solid #ebeef5;
padding-top: 12px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.result-header h3 {
margin: 0;
font-size: 15px;
}
.stream-events {
max-height: 400px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 6px;
}
.sync-result {
max-height: 500px;
overflow-y: auto;
}
.plan-json {
background: #f5f7fa;
padding: 12px;
border-radius: 6px;
font-size: 12px;
max-height: 300px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
}
.phase-output {
padding: 8px;
}
.output-content {
font-size: 13px;
line-height: 1.7;
max-height: 400px;
overflow-y: auto;
}
.output-content :deep(pre) {
background: #f5f7fa;
padding: 12px;
border-radius: 6px;
overflow: auto;
font-size: 12px;
}
.output-content :deep(code) {
background: #f5f7fa;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
.phase-start {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #ecf5ff;
border-radius: 6px;
font-size: 13px;
color: #409eff;
}
</style>