feat: group project workflow by batch
This commit is contained in:
26
agent.md
26
agent.md
@@ -79,8 +79,7 @@
|
||||
- 视觉方向是“温和的本地工作台 + 编辑器质感”,不要做成工业监控大屏。
|
||||
- 不要使用大面积黑底荧光绿、密集硬边框、无解释的红绿灯状态或全页面等宽字体。
|
||||
- 页面必须清楚区分:
|
||||
- 项目视图:项目里的智能体执行情况。
|
||||
- 工作流视图:阶段进度、交接、审查循环、主智能体监管。
|
||||
- 项目视图:项目里的智能体执行情况、工作流批次、阶段进度、交接、审查循环、主智能体监管。
|
||||
- 智能体视图:名称、描述、角色设定编辑。
|
||||
- 草稿:TOML 校验、diff、备份、写回。
|
||||
- 设置:Codex 路径和数据源配置。
|
||||
@@ -133,6 +132,7 @@
|
||||
## 工作流显示规则
|
||||
|
||||
- 工作流必须建模为动态事件流或有向图,不允许写死固定流程。
|
||||
- 项目内工作流必须先按“工作流批次”分组,再按阶段分组;同一项目的 `v1.0 初始搭建`、`v1.1 优化修复`、`v2.0 重构升级` 必须清楚区分。
|
||||
- `thread_spawn_edges` 是派发关系的主要结构化来源。
|
||||
- `threads` 表用于补充智能体昵称、角色、项目路径和更新时间。
|
||||
- `thread_goals` 用于补充目标状态。
|
||||
@@ -140,6 +140,28 @@
|
||||
- 没有计划文件时,只显示通用事件和推断,不强行命名阶段。
|
||||
- 所有推断必须标注来源和置信度。
|
||||
|
||||
### 子智能体派发元数据
|
||||
|
||||
主智能体派发任何子智能体任务时,必须在任务标题、目标或首段说明中保留以下元数据,方便本项目页面从 Codex 运行记录中准确归类:
|
||||
|
||||
```text
|
||||
[项目: /Users/yoilun/Code/codex-agent-manager]
|
||||
[工作流批次: v1.1 优化修复]
|
||||
[阶段: 优化阶段 1]
|
||||
[角色: 代码审查员]
|
||||
```
|
||||
|
||||
本项目当前已完成初始搭建后进入 `v1.1 优化修复`,当前优化改造归入 `优化阶段 1`。后续如果开启 `codex-agent-manager v2.0`,必须显式写成类似:
|
||||
|
||||
```text
|
||||
[项目: /Users/yoilun/Code/codex-agent-manager]
|
||||
[工作流批次: v2.0 重构升级]
|
||||
[阶段: 阶段 1 产品规划]
|
||||
[角色: 产品经理]
|
||||
```
|
||||
|
||||
如果继续派发下一级子智能体,必须把同一组元数据继续传递给下一级任务。`项目` 必须写完整项目路径,不要只写项目名;不要只写“继续优化”或“修一下页面”,否则页面只能低置信度推断批次和阶段。
|
||||
|
||||
## 状态规则
|
||||
|
||||
运行状态至少包含:
|
||||
|
||||
@@ -244,7 +244,9 @@ function describeRuntimeProcess(thread) {
|
||||
|
||||
export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath = '') {
|
||||
const targetPath = normalizePath(projectPath)
|
||||
const agents = runtime.agents.filter((agent) => belongsToProject(agent, targetPath))
|
||||
const agents = runtime.agents
|
||||
.filter((agent) => belongsToProject(agent, targetPath))
|
||||
.map((agent) => enrichProjectWorkflow(agent, targetPath))
|
||||
const threadIds = new Set(agents.map((agent) => agent.id))
|
||||
const agentByID = new Map(agents.map((agent) => [agent.id, agent]))
|
||||
const threadByID = new Map(runtime.threads.map((thread) => [thread.id, thread]))
|
||||
@@ -262,6 +264,7 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
||||
const handoffs = edges.map((edge) => normalizeProjectHandoff(edge, { agentByID, threadByID, source: runtime.source }))
|
||||
const phaseGroups = buildPhaseGroups(agents)
|
||||
const phaseHandoffs = buildPhaseHandoffs(handoffs, phaseGroups)
|
||||
const workflowBatches = buildWorkflowBatches(phaseHandoffs)
|
||||
const supervision = buildSupervision({ agents, handoffs, projectThreadIds, runtime, threadByID, goals })
|
||||
|
||||
return {
|
||||
@@ -273,6 +276,7 @@ export function filterRuntimeByProject(runtime = normalizeRuntime(), projectPath
|
||||
handoffs,
|
||||
phaseGroups,
|
||||
phaseHandoffs,
|
||||
workflowBatches,
|
||||
supervision,
|
||||
isEmpty: agents.length === 0,
|
||||
emptyTitle: '这个项目没有运行线程',
|
||||
@@ -301,6 +305,7 @@ function normalizeProjectHandoff(edge, context) {
|
||||
source: source.label,
|
||||
confidence: source.confidenceLabel,
|
||||
phaseName: handoffPhaseName({ fromAgent, toAgent, toIsMain }),
|
||||
workflowBatch: handoffWorkflowBatch({ fromAgent, toAgent, toIsMain }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +339,16 @@ function handoffPhaseName({ fromAgent, toAgent, toIsMain }) {
|
||||
return fromAgent?.phaseName || '未标注阶段'
|
||||
}
|
||||
|
||||
function handoffWorkflowBatch({ fromAgent, toAgent, toIsMain }) {
|
||||
if (toIsMain) {
|
||||
return fromAgent?.workflowBatch || '默认工作流'
|
||||
}
|
||||
if (toAgent?.workflowBatch) {
|
||||
return toAgent.workflowBatch
|
||||
}
|
||||
return fromAgent?.workflowBatch || '默认工作流'
|
||||
}
|
||||
|
||||
function handoffDirectionLabel({ fromAgent, toAgent, fromIsMain, toIsMain }) {
|
||||
if (fromIsMain && toAgent) {
|
||||
return '主线程派发'
|
||||
@@ -354,32 +369,38 @@ function buildPhaseGroups(agents) {
|
||||
const groups = new Map()
|
||||
for (const agent of agents) {
|
||||
const phaseName = agent.phaseName || extractPhaseName([agent.goal, ...(agent.projectHints || [])]) || '未标注阶段'
|
||||
if (!groups.has(phaseName)) {
|
||||
groups.set(phaseName, [])
|
||||
const workflowBatch = agent.workflowBatch || '默认工作流'
|
||||
const groupKey = `${workflowBatch}::${phaseName}`
|
||||
if (!groups.has(groupKey)) {
|
||||
groups.set(groupKey, { name: phaseName, workflowBatch, agents: [] })
|
||||
}
|
||||
groups.get(phaseName).push(agent)
|
||||
groups.get(groupKey).agents.push(agent)
|
||||
}
|
||||
return [...groups.entries()]
|
||||
.map(([name, phaseAgents]) => {
|
||||
return [...groups.values()]
|
||||
.map(({ name, workflowBatch, agents: phaseAgents }) => {
|
||||
const roles = [...new Set(phaseAgents.map((agent) => agent.role).filter(Boolean))].sort((a, b) => a.localeCompare(b, 'zh-CN'))
|
||||
return {
|
||||
name,
|
||||
workflowBatch,
|
||||
status: phaseStatus(phaseAgents),
|
||||
statusZh: formatStatus(phaseStatus(phaseAgents)),
|
||||
roles,
|
||||
agents: phaseAgents,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => phaseSortKey(a.name) - phaseSortKey(b.name) || a.name.localeCompare(b.name, 'zh-CN'))
|
||||
.sort(compareWorkflowPhase)
|
||||
}
|
||||
|
||||
function buildPhaseHandoffs(handoffs, phaseGroups) {
|
||||
const groups = new Map(phaseGroups.map((phase) => [phase.name, { ...phase, handoffs: [] }]))
|
||||
const groups = new Map(phaseGroups.map((phase) => [`${phase.workflowBatch}::${phase.name}`, { ...phase, handoffs: [] }]))
|
||||
for (const handoff of handoffs) {
|
||||
const phaseName = handoff.phaseName || '未标注阶段'
|
||||
if (!groups.has(phaseName)) {
|
||||
groups.set(phaseName, {
|
||||
const workflowBatch = handoff.workflowBatch || '默认工作流'
|
||||
const groupKey = `${workflowBatch}::${phaseName}`
|
||||
if (!groups.has(groupKey)) {
|
||||
groups.set(groupKey, {
|
||||
name: phaseName,
|
||||
workflowBatch,
|
||||
status: handoff.status,
|
||||
statusZh: formatStatus(handoff.status),
|
||||
roles: [],
|
||||
@@ -387,7 +408,7 @@ function buildPhaseHandoffs(handoffs, phaseGroups) {
|
||||
handoffs: [],
|
||||
})
|
||||
}
|
||||
groups.get(phaseName).handoffs.push(handoff)
|
||||
groups.get(groupKey).handoffs.push(handoff)
|
||||
}
|
||||
return [...groups.values()]
|
||||
.filter((phase) => phase.agents.length > 0 || phase.handoffs.length > 0)
|
||||
@@ -396,7 +417,36 @@ function buildPhaseHandoffs(handoffs, phaseGroups) {
|
||||
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'))
|
||||
.sort(compareWorkflowPhase)
|
||||
}
|
||||
|
||||
function buildWorkflowBatches(phases) {
|
||||
const groups = new Map()
|
||||
for (const phase of phases) {
|
||||
const batchName = phase.workflowBatch || '默认工作流'
|
||||
if (!groups.has(batchName)) {
|
||||
groups.set(batchName, {
|
||||
name: batchName,
|
||||
status: 'unknown',
|
||||
statusZh: '未知',
|
||||
phaseCount: 0,
|
||||
handoffCount: 0,
|
||||
agentCount: 0,
|
||||
phases: [],
|
||||
})
|
||||
}
|
||||
const batch = groups.get(batchName)
|
||||
batch.phases.push(phase)
|
||||
batch.phaseCount += 1
|
||||
batch.handoffCount += phase.handoffs.length
|
||||
batch.agentCount += phase.agents.length
|
||||
}
|
||||
return [...groups.values()]
|
||||
.map((batch) => {
|
||||
const status = phaseStatus(batch.phases)
|
||||
return { ...batch, status, statusZh: formatStatus(status) }
|
||||
})
|
||||
.sort((a, b) => workflowBatchSortKey(a.name) - workflowBatchSortKey(b.name) || a.name.localeCompare(b.name, 'zh-CN'))
|
||||
}
|
||||
|
||||
function buildSupervision({ agents, handoffs, projectThreadIds, runtime, threadByID, goals }) {
|
||||
@@ -434,6 +484,10 @@ function phaseStatus(agents) {
|
||||
function extractPhaseName(values) {
|
||||
for (const value of values) {
|
||||
const text = String(value || '')
|
||||
const optimization = text.match(/优化阶段\s*([0-9一二三四五六七八九十]+)/)
|
||||
if (optimization) {
|
||||
return `优化阶段 ${optimization[1]}`
|
||||
}
|
||||
const chinese = text.match(/阶段\s*([0-9一二三四五六七八九十]+)/)
|
||||
if (chinese) {
|
||||
return `阶段 ${chinese[1]}`
|
||||
@@ -446,6 +500,54 @@ function extractPhaseName(values) {
|
||||
return ''
|
||||
}
|
||||
|
||||
function enrichProjectWorkflow(agent, targetPath) {
|
||||
const values = [agent.goal, agent.name, agent.role, ...(agent.projectHints || [])]
|
||||
const explicitBatch = extractWorkflowBatchName(values)
|
||||
const explicitPhase = extractPhaseName(values)
|
||||
const isCurrentProject = basename(targetPath) === 'codex-agent-manager'
|
||||
let phaseName = explicitPhase || agent.phaseName || '未标注阶段'
|
||||
let workflowBatch = explicitBatch
|
||||
if (!workflowBatch && isCurrentProject && isOptimizationWork(values)) {
|
||||
if (!/^阶段\s*[0-9一二三四五六七八九十]+$/.test(phaseName) || /^优化阶段/.test(phaseName)) {
|
||||
workflowBatch = 'v1.1 优化修复'
|
||||
if (!explicitPhase || phaseName === '未标注阶段') {
|
||||
phaseName = '优化阶段 1'
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!workflowBatch && isCurrentProject && /^阶段\s*[0-9一二三四五六七八九十]+$/.test(phaseName)) {
|
||||
workflowBatch = 'v1.0 初始搭建'
|
||||
}
|
||||
return {
|
||||
...agent,
|
||||
phaseName,
|
||||
workflowBatch: normalizeWorkflowBatchName(workflowBatch || '默认工作流'),
|
||||
}
|
||||
}
|
||||
|
||||
function extractWorkflowBatchName(values) {
|
||||
for (const value of values) {
|
||||
const text = String(value || '')
|
||||
const explicit = text.match(/工作流批次\s*[::]\s*([^\]\n;]+)/)
|
||||
if (explicit) {
|
||||
return explicit[1].trim()
|
||||
}
|
||||
const version = text.match(/\b(v[0-9]+(?:\.[0-9]+)+)\s*([^\]\n;,,]*)/i)
|
||||
if (version && /重构|升级|优化|修复|初始|搭建|发布/.test(version[2] || '')) {
|
||||
return `${version[1]} ${version[2].trim()}`.trim()
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function normalizeWorkflowBatchName(value) {
|
||||
return String(value || '默认工作流').replace(/^(v[0-9]+(?:\.[0-9]+)+)([^\s].*)$/, '$1 $2').trim()
|
||||
}
|
||||
|
||||
function isOptimizationWork(values) {
|
||||
return values.some((value) => /优化|修复|改造|调整|合并|布局|详情切换|流程记录|工作流视图|工作流批次|新增|项目视图|交接/.test(String(value || '')))
|
||||
}
|
||||
|
||||
function actorDisplayName({ name = '', role = '', fallbackName = '', fallbackRole = '未记录角色' } = {}) {
|
||||
const cleanName = String(name || '').trim()
|
||||
const cleanRole = String(role || '').trim()
|
||||
@@ -462,13 +564,28 @@ function actorDisplayName({ name = '', role = '', fallbackName = '', fallbackRol
|
||||
}
|
||||
|
||||
function phaseSortKey(name) {
|
||||
const match = String(name).match(/阶段\s*([0-9]+)/)
|
||||
const match = String(name).match(/(?:阶段|优化阶段)\s*([0-9]+)/)
|
||||
if (!match) {
|
||||
return Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
return Number(match[1])
|
||||
}
|
||||
|
||||
function workflowBatchSortKey(name) {
|
||||
const match = String(name).match(/v([0-9]+)(?:\.([0-9]+))?/i)
|
||||
if (!match) {
|
||||
return Number.MAX_SAFE_INTEGER
|
||||
}
|
||||
return Number(match[1]) * 100 + Number(match[2] || 0)
|
||||
}
|
||||
|
||||
function compareWorkflowPhase(a, b) {
|
||||
return workflowBatchSortKey(a.workflowBatch) - workflowBatchSortKey(b.workflowBatch) ||
|
||||
a.workflowBatch.localeCompare(b.workflowBatch, 'zh-CN') ||
|
||||
phaseSortKey(a.name) - phaseSortKey(b.name) ||
|
||||
a.name.localeCompare(b.name, 'zh-CN')
|
||||
}
|
||||
|
||||
export function normalizeWorkflow(payload = {}) {
|
||||
const source = normalizeSource(payload.source)
|
||||
const phases = Array.isArray(payload.phases)
|
||||
@@ -624,7 +741,7 @@ function belongsToProject(agent, targetPath) {
|
||||
if (!agentPath || !targetPath.startsWith(`${agentPath}/`)) {
|
||||
return false
|
||||
}
|
||||
return [agent.process, ...(agent.projectHints || [])].some((value) => containsPathToken(value, targetPath))
|
||||
return [agent.process, ...(agent.projectHints || [])].some((value) => containsPathToken(value, targetPath) || containsProjectToken(value, targetPath))
|
||||
}
|
||||
|
||||
function threadBelongsToProject(thread, targetPath) {
|
||||
@@ -638,7 +755,7 @@ function threadBelongsToProject(thread, targetPath) {
|
||||
if (!cwd || !targetPath.startsWith(`${cwd}/`)) {
|
||||
return false
|
||||
}
|
||||
return [thread?.title, thread?.preview, thread?.agentPath].some((value) => containsPathToken(value, targetPath))
|
||||
return [thread?.title, thread?.preview, thread?.agentPath].some((value) => containsPathToken(value, targetPath) || containsProjectToken(value, targetPath))
|
||||
}
|
||||
|
||||
function threadCanJoinProjectFlow(thread, targetPath) {
|
||||
@@ -675,6 +792,19 @@ function containsPathToken(value, targetPath) {
|
||||
return false
|
||||
}
|
||||
|
||||
function containsProjectToken(value, targetPath) {
|
||||
if (!targetPath) {
|
||||
return false
|
||||
}
|
||||
const text = String(value || '')
|
||||
const metadata = text.match(/\[?项目\s*[::]\s*([^\]\n;]+)\]?/)
|
||||
if (!metadata) {
|
||||
return false
|
||||
}
|
||||
const candidate = normalizePath(metadata[1].trim())
|
||||
return candidate === targetPath
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) {
|
||||
return '没有时间记录'
|
||||
|
||||
@@ -320,6 +320,123 @@ test('bare subagent threads are not treated as main supervision threads', () =>
|
||||
assert.equal(projectRuntime.handoffs[0].directionLabel, '子智能体交接')
|
||||
})
|
||||
|
||||
test('groups project flow by workflow batch and classifies current optimizations', () => {
|
||||
const runtime = normalizeRuntime({
|
||||
items: [
|
||||
{
|
||||
id: 'main',
|
||||
cwd: '/Users/yoilun',
|
||||
title: '监管 /Users/yoilun/Code/codex-agent-manager 项目',
|
||||
threadSource: 'user',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'initial',
|
||||
cwd: '/Users/yoilun',
|
||||
title: '阶段 8:Docker 发布 /Users/yoilun/Code/codex-agent-manager',
|
||||
agentNickname: 'Ada',
|
||||
agentRole: '前端开发者',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'optimization',
|
||||
cwd: '/Users/yoilun',
|
||||
title: '优化改造:合并工作流视图到项目视图 /Users/yoilun/Code/codex-agent-manager',
|
||||
agentNickname: 'Rawls',
|
||||
agentRole: '代码审查员',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'v2',
|
||||
cwd: '/Users/yoilun',
|
||||
title: '[项目: /Users/yoilun/Code/codex-agent-manager] [工作流批次: v2.0 重构升级] [阶段: 阶段 3 后端架构]',
|
||||
agentNickname: 'Noether',
|
||||
agentRole: '后端架构师',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{ fromThreadId: 'main', toThreadId: 'initial', reason: 'closed' },
|
||||
{ fromThreadId: 'main', toThreadId: 'optimization', reason: 'open' },
|
||||
{ fromThreadId: 'optimization', toThreadId: 'v2', reason: 'open' },
|
||||
],
|
||||
source: { kind: 'sqlite_readonly', confidence: 'high' },
|
||||
})
|
||||
|
||||
const projectRuntime = filterRuntimeByProject(runtime, '/Users/yoilun/Code/codex-agent-manager')
|
||||
|
||||
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'initial').workflowBatch, 'v1.0 初始搭建')
|
||||
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'optimization').workflowBatch, 'v1.1 优化修复')
|
||||
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'optimization').phaseName, '优化阶段 1')
|
||||
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'v2').workflowBatch, 'v2.0 重构升级')
|
||||
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'v2').phaseName, '阶段 3')
|
||||
assert.deepEqual(projectRuntime.workflowBatches.map((batch) => batch.name), [
|
||||
'v1.0 初始搭建',
|
||||
'v1.1 优化修复',
|
||||
'v2.0 重构升级',
|
||||
])
|
||||
assert.deepEqual(projectRuntime.workflowBatches[1].phases.map((phase) => phase.name), ['优化阶段 1'])
|
||||
assert.deepEqual(projectRuntime.workflowBatches[1].phases[0].handoffs.map((handoff) => handoff.to), [
|
||||
'Rawls / 代码审查员',
|
||||
])
|
||||
})
|
||||
|
||||
test('workflow metadata requires full project path and keeps optimization phase name', () => {
|
||||
const runtime = normalizeRuntime({
|
||||
items: [
|
||||
{
|
||||
id: 'short-project',
|
||||
cwd: '/Users/yoilun',
|
||||
title: '[项目: codex-agent-manager] [工作流批次: v1.1 优化修复] [阶段: 优化阶段 1]',
|
||||
agentNickname: '短名项目',
|
||||
agentRole: '产品经理',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'full-project',
|
||||
cwd: '/Users/yoilun',
|
||||
title: '[项目: /Users/yoilun/Code/codex-agent-manager] [工作流批次: v1.1 优化修复] [阶段: 优化阶段 1]',
|
||||
agentNickname: '完整路径项目',
|
||||
agentRole: '产品经理',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'numeric-repair',
|
||||
cwd: '/Users/yoilun',
|
||||
title: '阶段 3:修复初始搭建问题 /Users/yoilun/Code/codex-agent-manager',
|
||||
agentNickname: '数字阶段',
|
||||
agentRole: '代码审查员',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
{
|
||||
id: 'new-batch',
|
||||
cwd: '/Users/yoilun',
|
||||
title: '新增工作流批次与项目视图交接 /Users/yoilun/Code/codex-agent-manager',
|
||||
agentNickname: '新增批次',
|
||||
agentRole: '前端开发者',
|
||||
threadSource: 'subagent',
|
||||
source: { kind: 'sqlite_table', confidence: 'high' },
|
||||
},
|
||||
],
|
||||
source: { kind: 'sqlite_readonly', confidence: 'high' },
|
||||
})
|
||||
|
||||
const projectRuntime = filterRuntimeByProject(runtime, '/Users/yoilun/Code/codex-agent-manager')
|
||||
|
||||
assert.deepEqual(projectRuntime.agents.map((agent) => agent.id), ['full-project', 'numeric-repair', 'new-batch'])
|
||||
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'full-project').phaseName, '优化阶段 1')
|
||||
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'full-project').workflowBatch, 'v1.1 优化修复')
|
||||
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'numeric-repair').workflowBatch, 'v1.0 初始搭建')
|
||||
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'new-batch').workflowBatch, 'v1.1 优化修复')
|
||||
assert.equal(projectRuntime.agents.find((agent) => agent.id === 'new-batch').phaseName, '优化阶段 1')
|
||||
})
|
||||
|
||||
test('does not turn ordinary conversation threads into agents', () => {
|
||||
const runtime = normalizeRuntime({
|
||||
items: [
|
||||
@@ -482,11 +599,22 @@ test('project and app views keep workflow inside the selected project view', asy
|
||||
assert.doesNotMatch(appSource, /工作流视图/)
|
||||
assert.match(viewSource, /selectedAgentId/)
|
||||
assert.match(viewSource, /selectAgent/)
|
||||
assert.match(viewSource, /phaseHandoffs/)
|
||||
assert.match(viewSource, /workflowBatches/)
|
||||
assert.match(viewSource, /工作流批次/)
|
||||
assert.match(viewSource, /supervision/)
|
||||
assert.match(viewSource, /goalSource/)
|
||||
})
|
||||
|
||||
test('agent collaboration guide requires workflow batch metadata', async () => {
|
||||
const thisFile = fileURLToPath(import.meta.url)
|
||||
const agentGuidePath = new URL('../../../agent.md', `file://${thisFile}`)
|
||||
const source = await readFile(agentGuidePath, 'utf8')
|
||||
|
||||
assert.match(source, /工作流批次/)
|
||||
assert.match(source, /v1\.1 优化修复/)
|
||||
assert.match(source, /阶段: 优化阶段 1/)
|
||||
})
|
||||
|
||||
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}`)
|
||||
|
||||
@@ -569,6 +569,37 @@ button {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.workflow-batches {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.workflow-batch {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.workflow-batch:first-child {
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.workflow-batch-heading {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.workflow-batch-heading span {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.phase-handoff-group {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -588,6 +619,11 @@ button {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.phase-handoff-nested {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.muted-line {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
|
||||
@@ -144,7 +144,7 @@ async function loadReadonlyData() {
|
||||
>
|
||||
<span role="cell">
|
||||
<strong>{{ agent.displayName }}</strong>
|
||||
<small>{{ agent.phaseName }}</small>
|
||||
<small>{{ agent.workflowBatch }} / {{ agent.phaseName }}</small>
|
||||
</span>
|
||||
<span role="cell">
|
||||
<StatusBadge :label="agent.statusZh" :status="agent.status" :source="agent.source" :confidence="agent.confidence" />
|
||||
@@ -163,12 +163,21 @@ async function loadReadonlyData() {
|
||||
<div class="panel-heading horizontal compact-heading">
|
||||
<div>
|
||||
<p class="eyebrow">阶段进度</p>
|
||||
<h3>项目内智能体阶段</h3>
|
||||
<h3>工作流批次与阶段</h3>
|
||||
</div>
|
||||
<span class="read-only-chip">{{ projectRuntime.phaseGroups.length }} 个阶段</span>
|
||||
<span class="read-only-chip">{{ projectRuntime.workflowBatches.length }} 个批次</span>
|
||||
</div>
|
||||
<ol v-if="projectRuntime.phaseGroups.length > 0" class="phase-list compact">
|
||||
<li v-for="phase in projectRuntime.phaseGroups" :key="phase.name" :data-status="phase.status">
|
||||
<div v-if="projectRuntime.workflowBatches.length > 0" class="workflow-batches">
|
||||
<section v-for="batch in projectRuntime.workflowBatches" :key="batch.name" class="workflow-batch">
|
||||
<div class="workflow-batch-heading">
|
||||
<div>
|
||||
<strong>{{ batch.name }}</strong>
|
||||
<span>{{ batch.phaseCount }} 个阶段 · {{ batch.agentCount }} 个智能体 · {{ batch.handoffCount }} 条交接</span>
|
||||
</div>
|
||||
<StatusBadge :label="batch.statusZh" :status="batch.status" source="SQLite 表" confidence="高" />
|
||||
</div>
|
||||
<ol class="phase-list compact">
|
||||
<li v-for="phase in batch.phases" :key="`${batch.name}-${phase.name}`" :data-status="phase.status">
|
||||
<div class="phase-dot" aria-hidden="true"></div>
|
||||
<div>
|
||||
<strong>{{ phase.name }}</strong>
|
||||
@@ -178,6 +187,8 @@ async function loadReadonlyData() {
|
||||
<StatusBadge :label="phase.statusZh" :status="phase.status" source="SQLite 表" confidence="高" />
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
<div v-else class="empty-state compact">
|
||||
<strong>没有阶段归属</strong>
|
||||
<p>当前项目智能体线程没有可解析的阶段标记。</p>
|
||||
@@ -192,15 +203,18 @@ async function loadReadonlyData() {
|
||||
</div>
|
||||
<span class="read-only-chip">{{ projectRuntime.handoffs.length }} 条交接</span>
|
||||
</div>
|
||||
<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 v-if="projectRuntime.workflowBatches.length > 0" class="phase-handoff-groups">
|
||||
<section v-for="batch in projectRuntime.workflowBatches" :key="batch.name" class="phase-handoff-group">
|
||||
<div class="phase-handoff-heading">
|
||||
<strong>{{ phase.name }}</strong>
|
||||
<StatusBadge :label="phase.statusZh" :status="phase.status" source="SQLite 表" confidence="高" />
|
||||
<strong>{{ batch.name }}</strong>
|
||||
<StatusBadge :label="batch.statusZh" :status="batch.status" source="SQLite 表" confidence="高" />
|
||||
</div>
|
||||
<section v-for="phase in batch.phases" :key="`${batch.name}-${phase.name}`" class="phase-handoff-nested">
|
||||
<strong>{{ phase.name }}</strong>
|
||||
<HandoffTimeline v-if="phase.handoffs.length > 0" :items="phase.handoffs" />
|
||||
<p v-else class="muted-line">这个阶段暂时没有交接边。</p>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
<div v-else class="empty-state compact">
|
||||
<strong>没有交接记录</strong>
|
||||
|
||||
Reference in New Issue
Block a user