fix: merge project workflow into project view
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import ProjectView from './views/ProjectView.vue'
|
import ProjectView from './views/ProjectView.vue'
|
||||||
import WorkflowView from './views/WorkflowView.vue'
|
|
||||||
import AgentView from './views/AgentView.vue'
|
import AgentView from './views/AgentView.vue'
|
||||||
import DraftsView from './views/DraftsView.vue'
|
import DraftsView from './views/DraftsView.vue'
|
||||||
import SettingsView from './views/SettingsView.vue'
|
import SettingsView from './views/SettingsView.vue'
|
||||||
@@ -9,7 +8,6 @@ import StatusBadge from './components/StatusBadge.vue'
|
|||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'projects', label: '项目视图', component: ProjectView },
|
{ id: 'projects', label: '项目视图', component: ProjectView },
|
||||||
{ id: 'workflow', label: '工作流视图', component: WorkflowView },
|
|
||||||
{ id: 'agents', label: '智能体视图', component: AgentView },
|
{ id: 'agents', label: '智能体视图', component: AgentView },
|
||||||
{ id: 'drafts', label: '草稿', component: DraftsView },
|
{ id: 'drafts', label: '草稿', component: DraftsView },
|
||||||
{ id: 'settings', label: '设置', component: SettingsView },
|
{ id: 'settings', label: '设置', component: SettingsView },
|
||||||
@@ -30,7 +28,7 @@ const activeComponent = computed(() => tabs.find((tab) => tab.id === activeTab.v
|
|||||||
<div class="connection-card" aria-label="连接状态">
|
<div class="connection-card" aria-label="连接状态">
|
||||||
<StatusBadge label="安全接口" status="complete" confidence="中" source="计划文件" />
|
<StatusBadge label="安全接口" status="complete" confidence="中" source="计划文件" />
|
||||||
<strong>按需读取和确认写回</strong>
|
<strong>按需读取和确认写回</strong>
|
||||||
<span>项目、运行线程和工作流保持只读;智能体草稿仅在校验、备份和确认后单文件写回。</span>
|
<span>项目、运行线程和项目内工作流保持只读;智能体草稿仅在校验、备份和确认后单文件写回。</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -195,6 +195,11 @@ export function normalizeRuntime(payload = {}) {
|
|||||||
source: source.label,
|
source: source.label,
|
||||||
confidence: source.confidenceLabel,
|
confidence: source.confidenceLabel,
|
||||||
lastActivity: thread.updatedAt || thread.createdAt || '没有时间记录',
|
lastActivity: thread.updatedAt || thread.createdAt || '没有时间记录',
|
||||||
|
displayName: actorDisplayName({
|
||||||
|
name: thread.agentNickname,
|
||||||
|
role: thread.agentRole || thread.role,
|
||||||
|
}),
|
||||||
|
phaseName: extractPhaseName([goalText, taskSummary, thread.title, thread.preview, thread.agentPath]) || '未标注阶段',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const source = normalizeSource(payload.source)
|
const source = normalizeSource(payload.source)
|
||||||
@@ -243,9 +248,12 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
|||||||
const threadIds = new Set(agents.map((agent) => agent.id))
|
const threadIds = new Set(agents.map((agent) => agent.id))
|
||||||
const agentByID = new Map(agents.map((agent) => [agent.id, agent]))
|
const agentByID = new Map(agents.map((agent) => [agent.id, agent]))
|
||||||
const threadByID = new Map(runtime.threads.map((thread) => [thread.id, thread]))
|
const threadByID = new Map(runtime.threads.map((thread) => [thread.id, thread]))
|
||||||
const projectThreadIds = new Set(runtime.threads.filter((thread) => threadCanJoinProjectFlow(thread, targetPath)).map((thread) => thread.id))
|
const projectThreadIds = new Set([
|
||||||
|
...threadIds,
|
||||||
|
...runtime.threads.filter((thread) => threadBelongsToProject(thread, targetPath)).map((thread) => thread.id),
|
||||||
|
])
|
||||||
const threads = runtime.threads.filter((thread) => threadIds.has(thread.id))
|
const threads = runtime.threads.filter((thread) => threadIds.has(thread.id))
|
||||||
const goals = runtime.goals.filter((goal) => threadIds.has(goal.threadId))
|
const goals = runtime.goals.filter((goal) => threadIds.has(goal.threadId) || projectThreadIds.has(goal.threadId))
|
||||||
const edges = runtime.edges.filter((edge) =>
|
const edges = runtime.edges.filter((edge) =>
|
||||||
(threadIds.has(edge.fromThreadId) || threadIds.has(edge.toThreadId)) &&
|
(threadIds.has(edge.fromThreadId) || threadIds.has(edge.toThreadId)) &&
|
||||||
projectThreadIds.has(edge.fromThreadId) &&
|
projectThreadIds.has(edge.fromThreadId) &&
|
||||||
@@ -253,6 +261,8 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
|||||||
)
|
)
|
||||||
const handoffs = edges.map((edge) => normalizeProjectHandoff(edge, { agentByID, threadByID, source: runtime.source }))
|
const handoffs = edges.map((edge) => normalizeProjectHandoff(edge, { agentByID, threadByID, source: runtime.source }))
|
||||||
const phaseGroups = buildPhaseGroups(agents)
|
const phaseGroups = buildPhaseGroups(agents)
|
||||||
|
const phaseHandoffs = buildPhaseHandoffs(handoffs, phaseGroups)
|
||||||
|
const supervision = buildSupervision({ agents, handoffs, projectThreadIds, runtime, threadByID, goals })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...runtime,
|
...runtime,
|
||||||
@@ -262,6 +272,8 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
|||||||
edges,
|
edges,
|
||||||
handoffs,
|
handoffs,
|
||||||
phaseGroups,
|
phaseGroups,
|
||||||
|
phaseHandoffs,
|
||||||
|
supervision,
|
||||||
isEmpty: agents.length === 0,
|
isEmpty: agents.length === 0,
|
||||||
emptyTitle: '这个项目没有运行线程',
|
emptyTitle: '这个项目没有运行线程',
|
||||||
emptyText: projectPath
|
emptyText: projectPath
|
||||||
@@ -273,14 +285,22 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
|||||||
function normalizeProjectHandoff(edge, context) {
|
function normalizeProjectHandoff(edge, context) {
|
||||||
const source = normalizeSource(edge.source, context.source)
|
const source = normalizeSource(edge.source, context.source)
|
||||||
const status = normalizeSpawnStatus(edge.reason || edge.status) || 'unknown'
|
const status = normalizeSpawnStatus(edge.reason || edge.status) || 'unknown'
|
||||||
|
const fromAgent = context.agentByID.get(edge.fromThreadId)
|
||||||
|
const toAgent = context.agentByID.get(edge.toThreadId)
|
||||||
|
const fromIsMain = isMainThread(context.threadByID.get(edge.fromThreadId))
|
||||||
|
const toIsMain = isMainThread(context.threadByID.get(edge.toThreadId))
|
||||||
|
const directionLabel = handoffDirectionLabel({ fromAgent, toAgent, fromIsMain, toIsMain })
|
||||||
return {
|
return {
|
||||||
|
id: `${edge.fromThreadId || 'unknown'}-${edge.toThreadId || 'unknown'}-${edge.createdAt || edge.reason || edge.status || 'event'}`,
|
||||||
from: runtimeNodeName(edge.fromThreadId, context),
|
from: runtimeNodeName(edge.fromThreadId, context),
|
||||||
to: runtimeNodeName(edge.toThreadId, context),
|
to: runtimeNodeName(edge.toThreadId, context),
|
||||||
summary: formatStatus(status),
|
summary: formatStatus(status),
|
||||||
|
directionLabel,
|
||||||
status,
|
status,
|
||||||
time: edge.createdAt || '后端事件',
|
time: edge.createdAt || '后端事件',
|
||||||
source: source.label,
|
source: source.label,
|
||||||
confidence: source.confidenceLabel,
|
confidence: source.confidenceLabel,
|
||||||
|
phaseName: handoffPhaseName({ fromAgent, toAgent, toIsMain }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,19 +310,50 @@ function runtimeNodeName(threadID, { agentByID, threadByID }) {
|
|||||||
}
|
}
|
||||||
const agent = agentByID.get(threadID)
|
const agent = agentByID.get(threadID)
|
||||||
if (agent) {
|
if (agent) {
|
||||||
return agent.name
|
return agent.displayName
|
||||||
}
|
}
|
||||||
const thread = threadByID.get(threadID)
|
const thread = threadByID.get(threadID)
|
||||||
if (thread?.threadSource === 'user' || (!thread?.agentNickname && !thread?.agentRole && !thread?.agentPath && !thread?.role)) {
|
if (isMainThread(thread)) {
|
||||||
return '主线程'
|
return '主线程 / 主智能体监管'
|
||||||
}
|
}
|
||||||
return thread?.agentNickname || thread?.agentRole || thread?.role || '未知线程'
|
return actorDisplayName({
|
||||||
|
name: thread?.agentNickname,
|
||||||
|
role: thread?.agentRole || thread?.role,
|
||||||
|
fallbackName: '未知线程',
|
||||||
|
fallbackRole: '角色未知',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handoffPhaseName({ fromAgent, toAgent, toIsMain }) {
|
||||||
|
if (toIsMain) {
|
||||||
|
return fromAgent?.phaseName || '未标注阶段'
|
||||||
|
}
|
||||||
|
if (toAgent?.phaseName) {
|
||||||
|
return toAgent.phaseName
|
||||||
|
}
|
||||||
|
return fromAgent?.phaseName || '未标注阶段'
|
||||||
|
}
|
||||||
|
|
||||||
|
function handoffDirectionLabel({ fromAgent, toAgent, fromIsMain, toIsMain }) {
|
||||||
|
if (fromIsMain && toAgent) {
|
||||||
|
return '主线程派发'
|
||||||
|
}
|
||||||
|
if (fromAgent && toIsMain) {
|
||||||
|
return '回到主线程'
|
||||||
|
}
|
||||||
|
if (fromAgent && toAgent && fromAgent.phaseName !== toAgent.phaseName) {
|
||||||
|
return '跨阶段交接'
|
||||||
|
}
|
||||||
|
if (fromAgent && toAgent) {
|
||||||
|
return '子智能体交接'
|
||||||
|
}
|
||||||
|
return '线程交接'
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPhaseGroups(agents) {
|
function buildPhaseGroups(agents) {
|
||||||
const groups = new Map()
|
const groups = new Map()
|
||||||
for (const agent of agents) {
|
for (const agent of agents) {
|
||||||
const phaseName = extractPhaseName([agent.goal, ...(agent.projectHints || [])]) || '未标注阶段'
|
const phaseName = agent.phaseName || extractPhaseName([agent.goal, ...(agent.projectHints || [])]) || '未标注阶段'
|
||||||
if (!groups.has(phaseName)) {
|
if (!groups.has(phaseName)) {
|
||||||
groups.set(phaseName, [])
|
groups.set(phaseName, [])
|
||||||
}
|
}
|
||||||
@@ -322,6 +373,54 @@ function buildPhaseGroups(agents) {
|
|||||||
.sort((a, b) => phaseSortKey(a.name) - phaseSortKey(b.name) || a.name.localeCompare(b.name, 'zh-CN'))
|
.sort((a, b) => phaseSortKey(a.name) - phaseSortKey(b.name) || a.name.localeCompare(b.name, 'zh-CN'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildPhaseHandoffs(handoffs, phaseGroups) {
|
||||||
|
const groups = new Map(phaseGroups.map((phase) => [phase.name, { ...phase, handoffs: [] }]))
|
||||||
|
for (const handoff of handoffs) {
|
||||||
|
const phaseName = handoff.phaseName || '未标注阶段'
|
||||||
|
if (!groups.has(phaseName)) {
|
||||||
|
groups.set(phaseName, {
|
||||||
|
name: phaseName,
|
||||||
|
status: handoff.status,
|
||||||
|
statusZh: formatStatus(handoff.status),
|
||||||
|
roles: [],
|
||||||
|
agents: [],
|
||||||
|
handoffs: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
groups.get(phaseName).handoffs.push(handoff)
|
||||||
|
}
|
||||||
|
return [...groups.values()]
|
||||||
|
.filter((phase) => phase.agents.length > 0 || phase.handoffs.length > 0)
|
||||||
|
.map((phase) => ({
|
||||||
|
...phase,
|
||||||
|
status: phaseStatus([...phase.agents, ...phase.handoffs]),
|
||||||
|
statusZh: formatStatus(phaseStatus([...phase.agents, ...phase.handoffs])),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => phaseSortKey(a.name) - phaseSortKey(b.name) || a.name.localeCompare(b.name, 'zh-CN'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSupervision({ agents, handoffs, projectThreadIds, runtime, threadByID, goals }) {
|
||||||
|
const mainThread = [...projectThreadIds]
|
||||||
|
.map((threadID) => threadByID.get(threadID))
|
||||||
|
.find((thread) => isMainThread(thread))
|
||||||
|
const orchestrator = agents.find((agent) => /编排|主智能体|监管/.test(`${agent.role} ${agent.name} ${agent.goal}`))
|
||||||
|
const supervisorID = mainThread?.id || orchestrator?.id || ''
|
||||||
|
const supervisorGoal = goals.find((goal) => goal.threadId === supervisorID) || goals.find((goal) => projectThreadIds.has(goal.threadId))
|
||||||
|
const status = mainThread?.status || orchestrator?.status || supervisorGoal?.status || (handoffs.length > 0 ? 'recent' : 'unknown')
|
||||||
|
const source = normalizeSource(mainThread?.source || orchestrator?.sourceDetail, runtime.source)
|
||||||
|
return {
|
||||||
|
actor: mainThread ? '主线程 / 主智能体监管' : orchestrator?.displayName || '主线程 / 主智能体监管',
|
||||||
|
status,
|
||||||
|
statusZh: formatStatus(status),
|
||||||
|
goal: supervisorGoal?.goal || supervisorGoal?.objective || orchestrator?.goal || '没有目标记录',
|
||||||
|
lastActivity: mainThread?.updatedAt || mainThread?.createdAt || supervisorGoal?.updatedAt || orchestrator?.lastActivity || '没有时间记录',
|
||||||
|
handoffCount: handoffs.length,
|
||||||
|
agentCount: agents.length,
|
||||||
|
source: source.label,
|
||||||
|
confidence: source.confidenceLabel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function phaseStatus(agents) {
|
function phaseStatus(agents) {
|
||||||
if (agents.some((agent) => agent.status === 'running' || agent.status === 'active')) {
|
if (agents.some((agent) => agent.status === 'running' || agent.status === 'active')) {
|
||||||
return 'running'
|
return 'running'
|
||||||
@@ -347,6 +446,21 @@ function extractPhaseName(values) {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function actorDisplayName({ name = '', role = '', fallbackName = '', fallbackRole = '未记录角色' } = {}) {
|
||||||
|
const cleanName = String(name || '').trim()
|
||||||
|
const cleanRole = String(role || '').trim()
|
||||||
|
if (cleanName && cleanRole) {
|
||||||
|
return `${cleanName} / ${cleanRole}`
|
||||||
|
}
|
||||||
|
if (cleanRole) {
|
||||||
|
return cleanRole
|
||||||
|
}
|
||||||
|
if (cleanName) {
|
||||||
|
return `${cleanName} / ${fallbackRole}`
|
||||||
|
}
|
||||||
|
return fallbackName ? `${fallbackName} / ${fallbackRole}` : `未知线程 / ${fallbackRole}`
|
||||||
|
}
|
||||||
|
|
||||||
function phaseSortKey(name) {
|
function phaseSortKey(name) {
|
||||||
const match = String(name).match(/阶段\s*([0-9]+)/)
|
const match = String(name).match(/阶段\s*([0-9]+)/)
|
||||||
if (!match) {
|
if (!match) {
|
||||||
@@ -528,11 +642,14 @@ function threadBelongsToProject(thread, targetPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function threadCanJoinProjectFlow(thread, targetPath) {
|
function threadCanJoinProjectFlow(thread, targetPath) {
|
||||||
if (threadBelongsToProject(thread, targetPath)) {
|
return threadBelongsToProject(thread, targetPath)
|
||||||
return true
|
}
|
||||||
|
|
||||||
|
function isMainThread(thread) {
|
||||||
|
if (!thread || thread.threadSource === 'subagent') {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
const cwd = normalizePath(thread?.cwd)
|
return Boolean(thread.threadSource === 'user' || (!thread.agentNickname && !thread.agentRole && !thread.agentPath && !thread.role))
|
||||||
return Boolean(cwd && targetPath.startsWith(`${cwd}/`))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSpawnStatus(status) {
|
function normalizeSpawnStatus(status) {
|
||||||
|
|||||||
@@ -202,6 +202,124 @@ test('filters runtime to selected project and displays agent names from Codex me
|
|||||||
assert.equal(projectRuntime.agents[0].statusZh, '运行中')
|
assert.equal(projectRuntime.agents[0].statusZh, '运行中')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('agent display names include role without inventing a nickname', () => {
|
||||||
|
const runtime = normalizeRuntime({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'role-only',
|
||||||
|
cwd: '/repo/a',
|
||||||
|
title: '阶段 1:产品规划',
|
||||||
|
agentRole: '产品经理',
|
||||||
|
threadSource: 'subagent',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'name-only',
|
||||||
|
cwd: '/repo/a',
|
||||||
|
title: '阶段 1:补充资料',
|
||||||
|
agentNickname: 'Nash',
|
||||||
|
threadSource: 'subagent',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
source: { kind: 'sqlite_readonly', confidence: 'high' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const projectRuntime = filterRuntimeByProject(runtime, '/repo/a')
|
||||||
|
|
||||||
|
assert.deepEqual(projectRuntime.agents.map((agent) => agent.displayName), [
|
||||||
|
'产品经理',
|
||||||
|
'Nash / 未记录角色',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('supervision only uses project-proven main threads', () => {
|
||||||
|
const runtime = normalizeRuntime({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'wrong-main',
|
||||||
|
cwd: '/Users/yoilun',
|
||||||
|
title: '普通对话,没有项目路径',
|
||||||
|
threadSource: 'user',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'right-main',
|
||||||
|
cwd: '/Users/yoilun',
|
||||||
|
title: '监管 /Users/yoilun/Code/codex-agent-manager 项目',
|
||||||
|
threadSource: 'user',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'coder',
|
||||||
|
cwd: '/Users/yoilun',
|
||||||
|
title: '阶段 2:实现 /Users/yoilun/Code/codex-agent-manager',
|
||||||
|
agentNickname: 'Ada',
|
||||||
|
agentRole: '前端开发者',
|
||||||
|
threadSource: 'subagent',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
goals: [
|
||||||
|
{ threadId: 'wrong-main', goal: '监管别的项目', status: 'active' },
|
||||||
|
{ threadId: 'right-main', goal: '监管 codex-agent-manager 项目', status: 'active' },
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{ fromThreadId: 'wrong-main', toThreadId: 'coder', reason: 'open' },
|
||||||
|
{ fromThreadId: 'right-main', toThreadId: 'coder', reason: 'open' },
|
||||||
|
],
|
||||||
|
source: { kind: 'sqlite_readonly', confidence: 'high' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const projectRuntime = filterRuntimeByProject(runtime, '/Users/yoilun/Code/codex-agent-manager')
|
||||||
|
|
||||||
|
assert.deepEqual(projectRuntime.handoffs.map((handoff) => handoff.from), ['主线程 / 主智能体监管'])
|
||||||
|
assert.equal(projectRuntime.supervision.goal, '监管 codex-agent-manager 项目')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bare subagent threads are not treated as main supervision threads', () => {
|
||||||
|
const runtime = normalizeRuntime({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'main',
|
||||||
|
cwd: '/repo/a',
|
||||||
|
title: '监管 /repo/a 项目',
|
||||||
|
threadSource: 'user',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bare-sub',
|
||||||
|
cwd: '/repo/a',
|
||||||
|
title: '阶段 3:没有角色元数据的子智能体',
|
||||||
|
threadSource: 'subagent',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reviewer',
|
||||||
|
cwd: '/repo/a',
|
||||||
|
title: '阶段 3:审查',
|
||||||
|
agentNickname: 'Rawls',
|
||||||
|
agentRole: '代码审查员',
|
||||||
|
threadSource: 'subagent',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
goals: [
|
||||||
|
{ threadId: 'main', goal: '监管真实项目', status: 'active' },
|
||||||
|
{ threadId: 'bare-sub', goal: '裸子智能体目标', status: 'active' },
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
{ fromThreadId: 'bare-sub', toThreadId: 'reviewer', reason: 'open' },
|
||||||
|
],
|
||||||
|
source: { kind: 'sqlite_readonly', confidence: 'high' },
|
||||||
|
})
|
||||||
|
|
||||||
|
const projectRuntime = filterRuntimeByProject(runtime, '/repo/a')
|
||||||
|
|
||||||
|
assert.equal(projectRuntime.supervision.goal, '监管真实项目')
|
||||||
|
assert.equal(projectRuntime.handoffs[0].directionLabel, '子智能体交接')
|
||||||
|
})
|
||||||
|
|
||||||
test('does not turn ordinary conversation threads into agents', () => {
|
test('does not turn ordinary conversation threads into agents', () => {
|
||||||
const runtime = normalizeRuntime({
|
const runtime = normalizeRuntime({
|
||||||
items: [
|
items: [
|
||||||
@@ -275,6 +393,15 @@ test('builds project handoffs and phase groups from runtime edges and agent task
|
|||||||
threadSource: 'subagent',
|
threadSource: 'subagent',
|
||||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'publisher',
|
||||||
|
cwd: '/repo/a',
|
||||||
|
title: '阶段 7:发布变更',
|
||||||
|
agentNickname: 'Noether',
|
||||||
|
agentRole: '发布经理',
|
||||||
|
threadSource: 'subagent',
|
||||||
|
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'other-main',
|
id: 'other-main',
|
||||||
cwd: '/repo/b',
|
cwd: '/repo/b',
|
||||||
@@ -287,8 +414,13 @@ test('builds project handoffs and phase groups from runtime edges and agent task
|
|||||||
{ fromThreadId: 'main', toThreadId: 'coder', reason: 'closed' },
|
{ fromThreadId: 'main', toThreadId: 'coder', reason: 'closed' },
|
||||||
{ fromThreadId: 'coder', toThreadId: 'reviewer', reason: 'open' },
|
{ fromThreadId: 'coder', toThreadId: 'reviewer', reason: 'open' },
|
||||||
{ fromThreadId: 'main', toThreadId: 'pm', reason: 'closed' },
|
{ fromThreadId: 'main', toThreadId: 'pm', reason: 'closed' },
|
||||||
|
{ fromThreadId: 'reviewer', toThreadId: 'main', reason: 'closed' },
|
||||||
|
{ fromThreadId: 'reviewer', toThreadId: 'publisher', reason: 'open' },
|
||||||
{ fromThreadId: 'other-main', toThreadId: 'coder', reason: 'open' },
|
{ fromThreadId: 'other-main', toThreadId: 'coder', reason: 'open' },
|
||||||
],
|
],
|
||||||
|
goals: [
|
||||||
|
{ threadId: 'main', goal: '监管 /repo/a 项目内智能体流程', status: 'active', updatedAt: '2026-05-26T09:00:00Z' },
|
||||||
|
],
|
||||||
source: { kind: 'sqlite_readonly', confidence: 'high' },
|
source: { kind: 'sqlite_readonly', confidence: 'high' },
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -298,27 +430,69 @@ test('builds project handoffs and phase groups from runtime edges and agent task
|
|||||||
'已完成',
|
'已完成',
|
||||||
'运行中',
|
'运行中',
|
||||||
'已完成',
|
'已完成',
|
||||||
|
'已完成',
|
||||||
|
'运行中',
|
||||||
])
|
])
|
||||||
assert.deepEqual(projectRuntime.handoffs.map((handoff) => `${handoff.from} -> ${handoff.to}`), [
|
assert.deepEqual(projectRuntime.handoffs.map((handoff) => `${handoff.from} -> ${handoff.to}`), [
|
||||||
'主线程 -> Averroes',
|
'主线程 / 主智能体监管 -> Averroes / 后端架构师',
|
||||||
'Averroes -> Rawls',
|
'Averroes / 后端架构师 -> Rawls / 代码审查员',
|
||||||
'主线程 -> Zeno',
|
'主线程 / 主智能体监管 -> Zeno / 高级项目经理',
|
||||||
|
'Rawls / 代码审查员 -> 主线程 / 主智能体监管',
|
||||||
|
'Rawls / 代码审查员 -> Noether / 发布经理',
|
||||||
|
])
|
||||||
|
assert.deepEqual(projectRuntime.handoffs.map((handoff) => handoff.directionLabel), [
|
||||||
|
'主线程派发',
|
||||||
|
'子智能体交接',
|
||||||
|
'主线程派发',
|
||||||
|
'回到主线程',
|
||||||
|
'跨阶段交接',
|
||||||
])
|
])
|
||||||
assert.ok(projectRuntime.handoffs.every((handoff) => handoff.time === '后端事件'))
|
assert.ok(projectRuntime.handoffs.every((handoff) => handoff.time === '后端事件'))
|
||||||
assert.deepEqual(projectRuntime.phaseGroups.map((phase) => phase.name), ['阶段 6', '阶段 7'])
|
assert.deepEqual(projectRuntime.phaseGroups.map((phase) => phase.name), ['阶段 6', '阶段 7'])
|
||||||
assert.equal(projectRuntime.phaseGroups[0].status, 'running')
|
assert.equal(projectRuntime.phaseGroups[0].status, 'running')
|
||||||
assert.deepEqual(projectRuntime.phaseGroups[0].roles, ['代码审查员', '后端架构师'])
|
assert.deepEqual(projectRuntime.phaseGroups[0].roles, ['代码审查员', '后端架构师'])
|
||||||
assert.deepEqual(projectRuntime.phaseGroups[0].agents.map((agent) => agent.name), ['Averroes', 'Rawls'])
|
assert.deepEqual(projectRuntime.phaseGroups[0].agents.map((agent) => agent.name), ['Averroes', 'Rawls'])
|
||||||
|
assert.deepEqual(projectRuntime.phaseGroups[0].agents.map((agent) => agent.displayName), ['Averroes / 后端架构师', 'Rawls / 代码审查员'])
|
||||||
|
assert.deepEqual(projectRuntime.phaseHandoffs.map((phase) => phase.name), ['阶段 6', '阶段 7'])
|
||||||
|
assert.deepEqual(projectRuntime.phaseHandoffs[0].handoffs.map((handoff) => `${handoff.from} -> ${handoff.to}`), [
|
||||||
|
'主线程 / 主智能体监管 -> Averroes / 后端架构师',
|
||||||
|
'Averroes / 后端架构师 -> Rawls / 代码审查员',
|
||||||
|
'Rawls / 代码审查员 -> 主线程 / 主智能体监管',
|
||||||
|
])
|
||||||
|
assert.deepEqual(projectRuntime.phaseHandoffs[1].handoffs.map((handoff) => `${handoff.from} -> ${handoff.to}`), [
|
||||||
|
'主线程 / 主智能体监管 -> Zeno / 高级项目经理',
|
||||||
|
'Rawls / 代码审查员 -> Noether / 发布经理',
|
||||||
|
])
|
||||||
|
assert.equal(projectRuntime.supervision.actor, '主线程 / 主智能体监管')
|
||||||
|
assert.equal(projectRuntime.supervision.goal, '监管 /repo/a 项目内智能体流程')
|
||||||
|
assert.equal(projectRuntime.supervision.handoffCount, 5)
|
||||||
|
assert.equal(projectRuntime.supervision.agentCount, 4)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('project view does not include sample runtime fallbacks', async () => {
|
test('project and app views keep workflow inside the selected project view', async () => {
|
||||||
const thisFile = fileURLToPath(import.meta.url)
|
const thisFile = fileURLToPath(import.meta.url)
|
||||||
const viewPath = new URL('../views/ProjectView.vue', `file://${thisFile}`)
|
const viewPath = new URL('../views/ProjectView.vue', `file://${thisFile}`)
|
||||||
const source = await readFile(viewPath, 'utf8')
|
const appPath = new URL('../App.vue', `file://${thisFile}`)
|
||||||
|
const viewSource = await readFile(viewPath, 'utf8')
|
||||||
|
const appSource = await readFile(appPath, 'utf8')
|
||||||
|
|
||||||
assert.doesNotMatch(source, /sampleProjects/)
|
assert.doesNotMatch(viewSource, /sampleProjects/)
|
||||||
assert.doesNotMatch(source, /sampleAgentMatrix/)
|
assert.doesNotMatch(viewSource, /sampleAgentMatrix/)
|
||||||
assert.match(source, /goalSource/)
|
assert.doesNotMatch(appSource, /WorkflowView/)
|
||||||
|
assert.doesNotMatch(appSource, /工作流视图/)
|
||||||
|
assert.match(viewSource, /selectedAgentId/)
|
||||||
|
assert.match(viewSource, /selectAgent/)
|
||||||
|
assert.match(viewSource, /phaseHandoffs/)
|
||||||
|
assert.match(viewSource, /supervision/)
|
||||||
|
assert.match(viewSource, /goalSource/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('handoff timeline uses normalized id as stable key', async () => {
|
||||||
|
const thisFile = fileURLToPath(import.meta.url)
|
||||||
|
const componentPath = new URL('../components/HandoffTimeline.vue', `file://${thisFile}`)
|
||||||
|
const source = await readFile(componentPath, 'utf8')
|
||||||
|
|
||||||
|
assert.match(source, /:key="item\.id \|\|/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('keeps project threads when Codex cwd is an ancestor but metadata names the project path', () => {
|
test('keeps project threads when Codex cwd is an ancestor but metadata names the project path', () => {
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ defineProps({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ol class="handoff-timeline">
|
<ol class="handoff-timeline">
|
||||||
<li v-for="item in items" :key="`${item.from}-${item.to}-${item.summary}`">
|
<li v-for="item in items" :key="item.id || `${item.from}-${item.to}-${item.summary}`">
|
||||||
<div class="timeline-marker" aria-hidden="true"></div>
|
<div class="timeline-marker" aria-hidden="true"></div>
|
||||||
<div>
|
<div>
|
||||||
<p class="timeline-title">{{ item.from }} → {{ item.to }}</p>
|
<p class="timeline-title">{{ item.from }} → {{ item.to }}</p>
|
||||||
<p>{{ item.summary }}</p>
|
<p>{{ item.summary }}</p>
|
||||||
<span>{{ item.time }} · 来源 {{ item.source }} · 置信度 {{ item.confidence }}</span>
|
<span>{{ item.directionLabel || '线程交接' }} · {{ item.time }} · 来源 {{ item.source }} · 置信度 {{ item.confidence }}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|||||||
@@ -295,6 +295,19 @@ button {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.matrix-row.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-row.clickable:hover,
|
||||||
|
.matrix-row.clickable.selected {
|
||||||
|
background: color-mix(in srgb, var(--green-soft) 62%, var(--panel));
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-row.clickable.selected {
|
||||||
|
box-shadow: inset 4px 0 0 var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
.matrix-row > span {
|
.matrix-row > span {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
@@ -438,6 +451,7 @@ button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.project-item[role="button"]:focus-visible,
|
.project-item[role="button"]:focus-visible,
|
||||||
|
.matrix-row[tabindex]:focus-visible,
|
||||||
.agent-list-item:focus-visible,
|
.agent-list-item:focus-visible,
|
||||||
.tab-button:focus-visible {
|
.tab-button:focus-visible {
|
||||||
outline: 3px solid color-mix(in srgb, var(--terracotta) 48%, transparent);
|
outline: 3px solid color-mix(in srgb, var(--terracotta) 48%, transparent);
|
||||||
@@ -550,6 +564,36 @@ button {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phase-handoff-groups {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-handoff-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-handoff-group:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phase-handoff-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted-line {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.58;
|
||||||
|
}
|
||||||
|
|
||||||
.graph-list {
|
.graph-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import HandoffTimeline from '../components/HandoffTimeline.vue'
|
import HandoffTimeline from '../components/HandoffTimeline.vue'
|
||||||
import StatusBadge from '../components/StatusBadge.vue'
|
import StatusBadge from '../components/StatusBadge.vue'
|
||||||
import { apiClient } from '../api/client'
|
import { apiClient } from '../api/client'
|
||||||
@@ -10,13 +10,34 @@ const error = ref('')
|
|||||||
const projectState = ref(normalizeProjects())
|
const projectState = ref(normalizeProjects())
|
||||||
const runtimeState = ref(normalizeRuntime())
|
const runtimeState = ref(normalizeRuntime())
|
||||||
const selectedProjectId = ref('')
|
const selectedProjectId = ref('')
|
||||||
|
const selectedAgentId = ref('')
|
||||||
|
|
||||||
const selectedProject = computed(() => projectState.value.projects.find((project) => project.id === selectedProjectId.value))
|
const selectedProject = computed(() => projectState.value.projects.find((project) => project.id === selectedProjectId.value))
|
||||||
const projectRuntime = computed(() => filterRuntimeByProject(runtimeState.value, selectedProject.value?.path || ''))
|
const projectRuntime = computed(() => filterRuntimeByProject(runtimeState.value, selectedProject.value?.path || ''))
|
||||||
const selectedAgent = computed(() => projectRuntime.value.agents[0])
|
const selectedAgent = computed(() =>
|
||||||
|
projectRuntime.value.agents.find((agent) => agent.id === selectedAgentId.value) ?? projectRuntime.value.agents[0],
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(loadReadonlyData)
|
onMounted(loadReadonlyData)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => projectRuntime.value.agents.map((agent) => agent.id).join('|'),
|
||||||
|
() => {
|
||||||
|
if (!projectRuntime.value.agents.some((agent) => agent.id === selectedAgentId.value)) {
|
||||||
|
selectedAgentId.value = projectRuntime.value.agents[0]?.id ?? ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function selectProject(projectId) {
|
||||||
|
selectedProjectId.value = projectId
|
||||||
|
selectedAgentId.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAgent(agent) {
|
||||||
|
selectedAgentId.value = agent.id
|
||||||
|
}
|
||||||
|
|
||||||
async function loadReadonlyData() {
|
async function loadReadonlyData() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
@@ -28,6 +49,7 @@ async function loadReadonlyData() {
|
|||||||
projectState.value = normalizeProjects(projectsPayload)
|
projectState.value = normalizeProjects(projectsPayload)
|
||||||
runtimeState.value = normalizeRuntime(runtimePayload)
|
runtimeState.value = normalizeRuntime(runtimePayload)
|
||||||
selectedProjectId.value = projectState.value.projects[0]?.id ?? ''
|
selectedProjectId.value = projectState.value.projects[0]?.id ?? ''
|
||||||
|
selectedAgentId.value = projectRuntime.value.agents[0]?.id ?? ''
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err?.message || '连接后端接口失败'
|
error.value = err?.message || '连接后端接口失败'
|
||||||
} finally {
|
} finally {
|
||||||
@@ -60,8 +82,8 @@ async function loadReadonlyData() {
|
|||||||
:class="{ selected: project.id === selectedProjectId }"
|
:class="{ selected: project.id === selectedProjectId }"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@click="selectedProjectId = project.id"
|
@click="selectProject(project.id)"
|
||||||
@keyup.enter="selectedProjectId = project.id"
|
@keyup.enter="selectProject(project.id)"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ project.name }}</strong>
|
<strong>{{ project.name }}</strong>
|
||||||
@@ -110,10 +132,19 @@ async function loadReadonlyData() {
|
|||||||
<span role="columnheader">进程</span>
|
<span role="columnheader">进程</span>
|
||||||
<span role="columnheader">最近活动</span>
|
<span role="columnheader">最近活动</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="agent in projectRuntime.agents" :key="agent.id" class="matrix-row" role="row">
|
<div
|
||||||
|
v-for="agent in projectRuntime.agents"
|
||||||
|
:key="agent.id"
|
||||||
|
class="matrix-row clickable"
|
||||||
|
:class="{ selected: agent.id === selectedAgentId }"
|
||||||
|
role="row"
|
||||||
|
tabindex="0"
|
||||||
|
@click="selectAgent(agent)"
|
||||||
|
@keyup.enter="selectAgent(agent)"
|
||||||
|
>
|
||||||
<span role="cell">
|
<span role="cell">
|
||||||
<strong>{{ agent.name }}</strong>
|
<strong>{{ agent.displayName }}</strong>
|
||||||
<small>{{ agent.role }}</small>
|
<small>{{ agent.phaseName }}</small>
|
||||||
</span>
|
</span>
|
||||||
<span role="cell">
|
<span role="cell">
|
||||||
<StatusBadge :label="agent.statusZh" :status="agent.status" :source="agent.source" :confidence="agent.confidence" />
|
<StatusBadge :label="agent.statusZh" :status="agent.status" :source="agent.source" :confidence="agent.confidence" />
|
||||||
@@ -142,7 +173,7 @@ async function loadReadonlyData() {
|
|||||||
<div>
|
<div>
|
||||||
<strong>{{ phase.name }}</strong>
|
<strong>{{ phase.name }}</strong>
|
||||||
<p>{{ phase.roles.join('、') || '未记录角色分类' }}</p>
|
<p>{{ phase.roles.join('、') || '未记录角色分类' }}</p>
|
||||||
<span>{{ phase.agents.map((agent) => agent.name).join('、') }}</span>
|
<span>{{ phase.agents.map((agent) => agent.displayName).join('、') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge :label="phase.statusZh" :status="phase.status" source="SQLite 表" confidence="高" />
|
<StatusBadge :label="phase.statusZh" :status="phase.status" source="SQLite 表" confidence="高" />
|
||||||
</li>
|
</li>
|
||||||
@@ -161,7 +192,16 @@ async function loadReadonlyData() {
|
|||||||
</div>
|
</div>
|
||||||
<span class="read-only-chip">{{ projectRuntime.handoffs.length }} 条交接</span>
|
<span class="read-only-chip">{{ projectRuntime.handoffs.length }} 条交接</span>
|
||||||
</div>
|
</div>
|
||||||
<HandoffTimeline v-if="projectRuntime.handoffs.length > 0" :items="projectRuntime.handoffs" />
|
<div v-if="projectRuntime.phaseHandoffs.length > 0" class="phase-handoff-groups">
|
||||||
|
<section v-for="phase in projectRuntime.phaseHandoffs" :key="phase.name" class="phase-handoff-group">
|
||||||
|
<div class="phase-handoff-heading">
|
||||||
|
<strong>{{ phase.name }}</strong>
|
||||||
|
<StatusBadge :label="phase.statusZh" :status="phase.status" source="SQLite 表" confidence="高" />
|
||||||
|
</div>
|
||||||
|
<HandoffTimeline v-if="phase.handoffs.length > 0" :items="phase.handoffs" />
|
||||||
|
<p v-else class="muted-line">这个阶段暂时没有交接边。</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
<div v-else class="empty-state compact">
|
<div v-else class="empty-state compact">
|
||||||
<strong>没有交接记录</strong>
|
<strong>没有交接记录</strong>
|
||||||
<p>当前项目没有匹配到主线程与智能体之间的交接边。</p>
|
<p>当前项目没有匹配到主线程与智能体之间的交接边。</p>
|
||||||
@@ -174,7 +214,7 @@ async function loadReadonlyData() {
|
|||||||
<aside class="panel detail-panel" aria-label="详情面板">
|
<aside class="panel detail-panel" aria-label="详情面板">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<p class="eyebrow">详情</p>
|
<p class="eyebrow">详情</p>
|
||||||
<h2>{{ selectedAgent?.name || selectedProject?.name || '只读详情' }}</h2>
|
<h2>{{ selectedAgent?.displayName || selectedProject?.name || '只读详情' }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
:label="selectedAgent?.statusZh || selectedProject?.statusZh || (error ? '连接失败' : '等待数据')"
|
:label="selectedAgent?.statusZh || selectedProject?.statusZh || (error ? '连接失败' : '等待数据')"
|
||||||
@@ -187,6 +227,11 @@ async function loadReadonlyData() {
|
|||||||
<p>{{ selectedAgent?.goal || '选择项目或等待运行线程后显示真实目标。' }}</p>
|
<p>{{ selectedAgent?.goal || '选择项目或等待运行线程后显示真实目标。' }}</p>
|
||||||
<span v-if="selectedAgent?.goalSource">{{ selectedAgent.goalSource }}</span>
|
<span v-if="selectedAgent?.goalSource">{{ selectedAgent.goalSource }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-block">
|
||||||
|
<h3>主智能体监管</h3>
|
||||||
|
<p>{{ projectRuntime.supervision.actor }}:{{ projectRuntime.supervision.goal }}</p>
|
||||||
|
<span>{{ projectRuntime.supervision.statusZh }} · 最近活动 {{ projectRuntime.supervision.lastActivity }}</span>
|
||||||
|
</div>
|
||||||
<div class="detail-block">
|
<div class="detail-block">
|
||||||
<h3>证据说明</h3>
|
<h3>证据说明</h3>
|
||||||
<p>数据来自只读接口。连接失败时保持错误状态,不使用示例数据伪装真实状态。</p>
|
<p>数据来自只读接口。连接失败时保持错误状态,不使用示例数据伪装真实状态。</p>
|
||||||
@@ -194,6 +239,8 @@ async function loadReadonlyData() {
|
|||||||
<div class="detail-grid">
|
<div class="detail-grid">
|
||||||
<span>项目数</span><strong>{{ projectState.projects.length }}</strong>
|
<span>项目数</span><strong>{{ projectState.projects.length }}</strong>
|
||||||
<span>当前项目线程数</span><strong>{{ projectRuntime.threads.length }}</strong>
|
<span>当前项目线程数</span><strong>{{ projectRuntime.threads.length }}</strong>
|
||||||
|
<span>子智能体数</span><strong>{{ projectRuntime.supervision.agentCount }}</strong>
|
||||||
|
<span>交接记录</span><strong>{{ projectRuntime.supervision.handoffCount }}</strong>
|
||||||
<span>接口</span><strong>{{ error ? '连接失败' : '只读连接' }}</strong>
|
<span>接口</span><strong>{{ error ? '连接失败' : '只读连接' }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
Reference in New Issue
Block a user