fix: derive run status from final result

This commit is contained in:
mesalogo
2026-08-15 21:17:17 +08:00
parent 939c3ab1b1
commit 0b96400e51
5 changed files with 99 additions and 8 deletions
+6
View File
@@ -53,6 +53,12 @@ Keep Electron security boundaries intact:
- Reuse installed libraries and shared contracts before adding dependencies.
- Keep changes focused. Do not add unrelated refactors or documentation.
- Add or update focused tests for behavioral changes and regressions.
- After completing any functional change, inspect the affected product,
architecture, design, feature, setup, and operational documentation and
update every relevant document to match the implemented behavior. Treat the
final code and validated runtime behavior as the source of truth: correct
stale documentation rather than preserving outdated intent. Avoid
documentation churn only when the change has no documented impact.
- Avoid broad catches that erase HTTP status, cancellation, or provider error
context.
- Keep UI accessible with labels, keyboard behavior, semantic roles, and visible
+2 -1
View File
@@ -505,7 +505,8 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- 使用 `dashboard` 壳层,并通过 `PageTabs` 提供“任务与会话 / 活动时间线 / 用量统计”三个同级视图。
- 默认视图按“项目 → 任务或会话 → 活动详情”组织,项目范围持续可见,任务或会话详情可以折叠。
- 活动时间线按项目分组、按任务或会话建立横向轨道,所有轨道共享同一执行顺序并按事件时间排列,以带身份名称的节点表达用户、主 Agent、子专家、工具、审批、状态和并行关系;选择节点后显示所属会话、不可变范围快照和完整详情
- 任务或会话的综合状态以最近一次顶层请求对应的最终 Agent 结果为准;最终结果尚未产生时使用该请求的当前状态。中间工具或子专家的失败、取消和中断保留在活动详情中,但不得覆盖最终成功状态
- 活动时间线按项目分组、按任务或会话建立横向轨道,所有轨道共享同一执行顺序并按事件时间排列,以带身份名称的节点表达用户、主 Agent、子专家、工具、审批、状态和并行关系。节点内使用 `U / G / S / T / A` 拉丁字母简称,节点下显示完整身份或名称;选择节点后显示所属会话、不可变范围快照和完整详情。
- 用量统计与活动记录分离,支持按项目、会话和模型切换统计维度,宽表格在独立容器内横向滚动。
- 活动状态筛选使用 `SegmentedControl`,不与页面页签混合。清空历史遵循破坏性操作政策。
@@ -26,7 +26,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
- 通道项目标识平台并确定默认工作目录、处理后端和默认模式。
- 远程会话标识具体发送者或群聊。
- 消息记录具体发送者和本次实际使用的模式。
- 任务与活动记录执行、工具调用和结果。
- 运行记录覆盖任务执行、工具调用和结果。
## 2. 已确认的产品决策
@@ -119,7 +119,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
| 通道项目 | 平台、连接状态、默认根目录、处理后端、默认模式 |
| 远程会话 | 平台账号、私聊用户或群聊、连续上下文 |
| 消息 | 具体发送者、本次实际模式、正文、时间和处理状态 |
| 任务与活动 | Runtime、工具调用、执行结果和错误 |
| 运行记录 | Runtime、工具调用、执行结果和错误 |
### 5.3 会话命名
@@ -268,7 +268,7 @@ Execute 启动前检查解析后的后端是否支持工具执行,并返回可
- 尚无远程会话时显示等待首条客户端消息的空状态和设置入口。
- 旧版本误建在通道项目中的普通本地会话不参与通道会话列表,但保留其数据。
- 远程会话底部说明客户端联动方式,只显示历史、任务和执行结果,不再提及已移除的审批流程。
-任务与活动”页面按会话分组显示远程任务;所有分组首次进入时默认收起,包括进行中、失败和已完成状态,用户可通过原生展开控件查看明细。
-运行记录”的“任务与会话”视图按“项目 → 任务或会话 → 活动详情”显示远程任务;所有任务或会话首次进入时默认收起,用户可通过原生展开控件查看明细。综合状态以最近一次顶层请求对应的最终 Agent 结果为准,最终结果尚未产生时使用请求当前状态;中间工具或子专家的失败、取消和中断不得覆盖最终成功状态
### 7.6 微信扫码绑定
@@ -672,7 +672,7 @@ Renderer 快照只返回是否已配置和脱敏标识。
- [ ] 切换到通道项目不会创建普通本地会话。
- [ ] 通道项目隐藏“新建对话”和 `Ctrl+N`,全局快捷命令也不创建会话。
- [ ] 没有远程会话时显示等待客户端首条消息的空状态。
- [ ]任务与活动”中的会话分组默认收起,进行中、失败和已完成状态行为一致
- [ ]运行记录”的任务或会话分组默认收起;综合状态使用最近一次顶层请求的最终 Agent 结果,中间工具或子专家失败不得覆盖最终成功状态
- [ ] 微信图片和文件显示在对应远程消息中,附件消息无需附带文字。
- [ ] 支持的附件进入所选后端现有图片或文档上下文;不支持和超限附件返回明确提示。
+52
View File
@@ -162,6 +162,58 @@ describe('ActivityPanel', () => {
).toHaveLength(2)
})
it('uses the latest top-level Agent result for conversation status', () => {
const request: ActivityRecord = {
...makeRecord(1, 'running'),
conversationId: 'conversation-status',
requestId: 'request-status',
kind: 'request',
title: '最终成功的任务'
}
const failedTool: ActivityRecord = {
...makeRecord(2, 'failed'),
conversationId: request.conversationId,
requestId: request.requestId,
kind: 'tool',
title: '中间失败的工具'
}
const cancelledSubagent: ActivityRecord = {
...makeRecord(3, 'cancelled'),
conversationId: request.conversationId,
requestId: request.requestId,
kind: 'subagent',
title: '已取消的子专家'
}
const result: ActivityRecord = {
...makeRecord(4, 'completed'),
conversationId: request.conversationId,
requestId: request.requestId,
kind: 'result',
title: '任务执行完成'
}
render(
<ActivityPanel
onClear={vi.fn()}
onOpenConversation={vi.fn()}
records={[result, cancelledSubagent, failedTool, request]}
tokenUsage={makeTokenUsage()}
/>
)
const conversation = screen
.getByText('对话:最终成功的任务')
.closest('summary')
expect(conversation).not.toBeNull()
if (!conversation) {
return
}
expect(within(conversation).getByText('已完成')).toBeInTheDocument()
expect(within(conversation).queryByText('失败')).not.toBeInTheDocument()
expect(
within(conversation).queryByText('已取消')
).not.toBeInTheDocument()
})
it('filters active and exceptional activity and opens its conversation', () => {
const onOpenConversation = vi.fn()
render(
+35 -3
View File
@@ -127,9 +127,32 @@ function activityScopeKey(scope: ActivityRecord['scope']): string {
function conversationStatus(
records: readonly ActivityRecord[]
): ActivityRecord['status'] {
const latestRecord = (
candidates: readonly ActivityRecord[]
): ActivityRecord | undefined =>
candidates.reduce<ActivityRecord | undefined>(
(latest, record) =>
!latest || record.createdAt > latest.createdAt ? record : latest,
undefined
)
const latestRequest = latestRecord(
records.filter((record) => record.kind === 'request')
)
if (latestRequest) {
const latestResult = latestRecord(
records.filter(
(record) =>
record.kind === 'result' &&
record.requestId === latestRequest.requestId
)
)
return latestResult?.status ?? latestRequest.status
}
return (
records.find(isActive)?.status ??
records.find(isFailed)?.status ??
latestRecord(records.filter((record) => record.kind === 'result'))
?.status ??
latestRecord(records)?.status ??
'completed'
)
}
@@ -154,6 +177,13 @@ function groupActivityRecordsByProject(
allRecords: readonly ActivityRecord[]
): ProjectActivityGroup[] {
const conversationTitles = getConversationTitles(allRecords)
const allConversationRecords = new Map<string, ActivityRecord[]>()
for (const record of allRecords) {
const conversationRecords =
allConversationRecords.get(record.conversationId) ?? []
conversationRecords.push(record)
allConversationRecords.set(record.conversationId, conversationRecords)
}
const projectGroups = new Map<
string,
{
@@ -186,7 +216,9 @@ function groupActivityRecordsByProject(
conversationTitles.get(conversationId) ?? items[0]!.title,
records: items,
latestAt: Math.max(...items.map((record) => record.createdAt)),
status: conversationStatus(items)
status: conversationStatus(
allConversationRecords.get(conversationId) ?? items
)
})
)
return {