fix(statusbar): 生信任务活跃期回退消息列表点亮 4 点
生产实测暴露缺口:生信对话的 chat/stream 在 plan_ready 后即关闭 (_finishCtx 清 streamCtx→currentMessage 为 null),而任务阶段恰在 流关闭后才真正运行——StatusBar 只读 currentMessage,阶段运行期 间 4 点仍不亮。这正是用户报的 bug 主场景。 - chat.js 新增 agentDotMessage getter:优先流式中 currentMessage; 流已收口时回退到消息列表里持有活跃生信任务(progress.jobId 且 jobStatus ∈ created/running/paused)的最后一条助手消息——其派生 pipeline 块正被任务事件订阅(connectJobEvents onEvent → patchBioflowEvent → syncBioflowDotBlock)持续刷新。 - StatusBar.vue 数据源换 agentDotMessage;TopBar.vue runningPhaseOf 同换 + aggregate 在生信任务活跃期(busy=false 但有 running 相位) 点亮聚合点。 - 单测补生产实测场景:流已收口任务活跃→回退点亮;阶段推进随动; 任务终态正确收口空态。 验证:r3debug 89/89(新增回退用例)、主套件 373/373、dist 重建。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -170,6 +170,9 @@ set -a
|
||||
set +a
|
||||
( cd "$ROOT/sdk" && setsid "$PYBIN" -m uvicorn app.main:app --host "$BIND" --port 8090 \
|
||||
> /tmp/plantai-sdk.log 2>&1 < /dev/null & )
|
||||
disown -a 2>/dev/null || true
|
||||
# 2026-09-12 部署实测:BFF 段缺 disown 时,脚本尾部 bash 隐式 wait 长驻 BFF 进程,
|
||||
# start_all 永不返回(vstore_restart.sh 有 disown 故不卡)。此处补上。
|
||||
echo "BFF 启动中(8090),5 秒后自检:"
|
||||
sleep 5
|
||||
curl -s --max-time 15 http://127.0.0.1:8090/api/health | head -c 300
|
||||
|
||||
@@ -17,7 +17,7 @@ const agentStates = computed(() => {
|
||||
try {
|
||||
const chat = chatStore.value;
|
||||
if (!chat) return null;
|
||||
const msg = chat.currentMessage;
|
||||
const msg = chat.agentDotMessage ?? chat.currentMessage;
|
||||
const pipe = (msg?.blocks || []).find((b) => b?.type === 'pipeline');
|
||||
const steps = pipe?.steps || [];
|
||||
const out = {};
|
||||
|
||||
@@ -72,6 +72,10 @@ const aggregate = computed(() => {
|
||||
const phase = runningPhaseOf(chat);
|
||||
return { phase, running: true, error: false };
|
||||
}
|
||||
// 生信任务活跃期(chat/stream 已收口、busy=false 但阶段仍在运行):
|
||||
// 派生 pipeline 块有 running 相位时聚合点同样点亮(2026-09-12 statusbar 修复)
|
||||
const phase = runningPhaseOf(chat);
|
||||
if (phase) return { phase, running: true, error: false };
|
||||
if (chat.lastResult && chat.lastResult.ok === false) {
|
||||
return { phase: null, running: false, error: true };
|
||||
}
|
||||
@@ -91,7 +95,7 @@ const aggregateClass = computed(() => {
|
||||
/** 从当前助手消息的 pipeline 块取正在运行相位的 key */
|
||||
function runningPhaseOf(chat) {
|
||||
try {
|
||||
const msg = chat.currentMessage;
|
||||
const msg = chat.agentDotMessage ?? chat.currentMessage;
|
||||
const pipe = (msg?.blocks || []).find((b) => b?.type === 'pipeline');
|
||||
const running = (pipe?.steps || []).find((s) => s?.state === 'running');
|
||||
if (running && running.phase && running.phase !== 'done') return running.phase;
|
||||
|
||||
@@ -530,6 +530,27 @@ export const useChatStore = defineStore('chat', {
|
||||
currentMessage() {
|
||||
return this._ctx?.aMsg || null;
|
||||
},
|
||||
/**
|
||||
* 状态栏 4 个 agent 点的数据源消息(2026-09-12 statusbar 修复):
|
||||
* 优先取流式中的 currentMessage;流已收口时(生信对话的 chat/stream 在
|
||||
* plan_ready 后就关闭、ctx 被 _finishCtx 清理,而任务阶段此刻才真正运行),
|
||||
* 回退到消息列表里仍持有活跃生信任务的最后一条助手消息——其派生 pipeline
|
||||
* 块正被任务事件订阅(connectJobEvents onEvent → patchBioflowEvent →
|
||||
* syncBioflowDotBlock)持续刷新。
|
||||
*/
|
||||
agentDotMessage() {
|
||||
const live = this._ctx?.aMsg;
|
||||
if (live) return live;
|
||||
const msgs = this.messages || [];
|
||||
const ACTIVE_JOB = new Set(['created', 'running', 'paused']);
|
||||
for (let i = msgs.length - 1; i >= 0; i -= 1) {
|
||||
const m = msgs[i];
|
||||
if (!m || m.role !== 'assistant' || m.kind !== 'bioflow' || !Array.isArray(m.blocks)) continue;
|
||||
const prog = m.blocks.find((b) => b && b.type === 'bioflow_progress');
|
||||
if (prog && prog.jobId && ACTIVE_JOB.has(prog.jobStatus)) return m;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
/** 当前会话的 progress 事件(B3 契约:阶段卡数据源)。
|
||||
* 流式中取 ctx.events;终态后 ctx 已清,取 lastEvents 供 bench 持续展示。 */
|
||||
currentEvents() {
|
||||
|
||||
@@ -20,7 +20,10 @@ vi.mock('../../src/stores/health.js', () => ({
|
||||
}));
|
||||
|
||||
import StatusBar from '../../src/components/layout/StatusBar.vue';
|
||||
import { useChatStore } from '../../src/stores/chat.js';
|
||||
import {
|
||||
useChatStore,
|
||||
syncBioflowDotBlock,
|
||||
} from '../../src/stores/chat.js';
|
||||
|
||||
const PHASES = ['planning', 'retrieval', 'evidence', 'generation'];
|
||||
|
||||
@@ -139,6 +142,52 @@ describe('StatusBar 4 个 agent 点', () => {
|
||||
expect(dotClasses(wrapper)).toEqual(['', '', '', '']);
|
||||
});
|
||||
|
||||
it('生信流已收口但任务活跃(2026-09-12 生产实测缺口)→ 回退消息列表点亮', async () => {
|
||||
const chat = useChatStore();
|
||||
// 模拟真实场景:chat/stream 已在 plan_ready 后关闭,_finishCtx 清理了 ctx
|
||||
// (streamCtx 无本会话键),但任务阶段仍在运行,派生块正被任务事件订阅刷新
|
||||
const aMsg = {
|
||||
id: 'm6', role: 'assistant', kind: 'bioflow', status: 'streaming',
|
||||
blocks: [
|
||||
{ type: 'bioflow_path', status: 'done', jobId: 'j2', candidates: [{ stages: ['a', 'b'] }], selected: 0 },
|
||||
{
|
||||
type: 'bioflow_progress', jobId: 'j2', jobStatus: 'running',
|
||||
stages: [
|
||||
{ stage: 'a', status: 'running' },
|
||||
{ stage: 'b', status: 'waiting' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'pipeline', derived: true, status: 'running',
|
||||
steps: PHASES.map((p) => step(p, p === 'planning' ? 'running' : 'waiting')),
|
||||
},
|
||||
],
|
||||
};
|
||||
chat.sessionId = 's-dots2';
|
||||
chat.messages = [aMsg];
|
||||
chat.sessionMsgs['s-dots2'] = [aMsg];
|
||||
chat.streamCtx = {}; // 关键:无活跃流(ctx 已被 _finishCtx 清理)
|
||||
expect(chat.agentDotMessage).toBe(chat.messages[0]); // getter 回退正确
|
||||
const wrapper = mount(StatusBar);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(dotClasses(wrapper)).toEqual(['running', '', '', '']);
|
||||
// 阶段推进:事件 patch 后派生块随动(模拟任务事件订阅 onEvent 链)
|
||||
const proxyMsg = chat.messages[0];
|
||||
const blk = proxyMsg.blocks.find((b) => b.type === 'bioflow_progress');
|
||||
blk.stages[0].status = 'done';
|
||||
blk.stages[1].status = 'running';
|
||||
syncBioflowDotBlock(proxyMsg);
|
||||
await wrapper.vm.$nextTick();
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
expect(dotClasses(wrapper)).toEqual(['done', 'running', '', '']);
|
||||
// 任务终态:jobStatus done → getter 不再回退,点全空态(正确收口)
|
||||
blk.jobStatus = 'done';
|
||||
await wrapper.vm.$nextTick();
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
expect(dotClasses(wrapper)).toEqual(['', '', '', '']);
|
||||
});
|
||||
|
||||
it('真实时序:先 mount(挂载完成)后流到达 → computed 响应式点亮', async () => {
|
||||
const chat = useChatStore();
|
||||
// 1. 先 mount,onMounted 动态 import 完成(与真实 app 启动时序一致)
|
||||
|
||||
Reference in New Issue
Block a user