diff --git a/apps/web/src/features/desk-task/__tests__/pending-cards.test.ts b/apps/web/src/features/desk-task/__tests__/pending-cards.test.ts index f39dbaa..ce6f3a6 100644 --- a/apps/web/src/features/desk-task/__tests__/pending-cards.test.ts +++ b/apps/web/src/features/desk-task/__tests__/pending-cards.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'; import { dataEntryCardSchema, decisionCardSchema } from '@contracts'; import type { TaskDetail } from '@contracts'; -import { pendingCardsOf } from '../internal/pending-cards.ts'; +import { awaitingHuman, pendingCardsOf } from '../internal/pending-cards.ts'; const AT = '2026-08-28T00:55:30.054Z'; @@ -66,3 +66,40 @@ describe('pendingCardsOf', () => { expect(pendingCardsOf(chat(entry('c-1'), later)).entryLedger).toBe('客户档案'); }); }); + +/** + * **有活等着人办就不摆推进场景**(2026-08-31 用户:「在中央工作区修改了内容点击保存后, + * 为什么会出现『AI 正在推进这单』」)。 + * + * 件上按保存走 `afterSave`:**先重推件、再叫醒任务**(「保存即铺格」)。叫醒把 runtime + * 拨成 `running`,而工作区的 `running` 判据压过 `viewHtml` —— 人刚填完按保存,屏上那份 + * 刚重推的件被推进场景顶掉几秒;而那一拍按「叫了人就等人」的闸根本不问模型,那一屏 + * 写的「看材料、写台账、调工具」一件没发生。这一组钉的就是那道新条件。 + */ +describe('awaitingHuman:这一单还有活等着人办吗', () => { + const none = { decision: false, entry: false } as const; + + it('没工单没卡 —— AI 真在埋头算,推进场景照旧摆(2026-08-21 要的正是它)', () => { + expect(awaitingHuman({ openOrders: 0, cards: none })).toBe(false); + }); + + it('有工单没办结 —— 人正要在件上填,不许拿推进场景盖掉件', () => { + expect(awaitingHuman({ openOrders: 1, cards: none })).toBe(true); + }); + + it('决策卡还没选 —— 人要决定的对象常常就是件上那份稿,件得留着', () => { + expect(awaitingHuman({ openOrders: 0, cards: { decision: true, entry: false } })).toBe(true); + }); + + it('录入卡还没确认 —— 让位给录入占位页那一支(它排在 running 之后)', () => { + expect(awaitingHuman({ openOrders: 0, cards: { decision: false, entry: true, entryLedger: '客户档案' } })).toBe(true); + }); + + it('工单全办结、卡全答过 —— 回到「AI 真在算」,推进场景该摆', () => { + expect(awaitingHuman({ openOrders: 0, cards: none })).toBe(false); + }); + + it('三样都在 —— 仍是「等人」,不是「AI 在推进」', () => { + expect(awaitingHuman({ openOrders: 2, cards: { decision: true, entry: true, entryLedger: '客户档案' } })).toBe(true); + }); +}); diff --git a/apps/web/src/features/desk-task/internal/ViewRegion.tsx b/apps/web/src/features/desk-task/internal/ViewRegion.tsx index 8b02924..e40814e 100644 --- a/apps/web/src/features/desk-task/internal/ViewRegion.tsx +++ b/apps/web/src/features/desk-task/internal/ViewRegion.tsx @@ -21,7 +21,7 @@ import ScrollArea from '@web-shared/ScrollArea.tsx'; import { EmptyState } from '@web-shared/desk/module-kit.tsx'; import { ViewBlocks, ViewContainer } from '@ui'; import { pagerFace, pagerText } from './pager.ts'; -import { pendingCardsOf } from './pending-cards.ts'; +import { awaitingHuman, pendingCardsOf } from './pending-cards.ts'; import type { ViewEdits, ViewSave } from '@ui'; import { GuideCard, WorkOrderReport } from './work-order-report.tsx'; import { FileViewer } from './FileViewer.tsx'; @@ -176,6 +176,8 @@ export function ViewRegion({ detail, onReplyOrder, values, onField, locate }: Vi */ const pending = pendingCardsOf(detail.chat); const pendingChatCard = pending.decision || pending.entry; + /* 这一单还有活等着人办吗 —— 推进场景要不要摆看它(缘由见 `awaitingHuman` 的头注) */ + const holding = awaitingHuman({ openOrders: openList.length, cards: pending }); /* * 标记已读 / 未读 —— 乐观更新交给失效:标完服务端广播 task 触点, @@ -763,7 +765,7 @@ export function ViewRegion({ detail, onReplyOrder, values, onField, locate }: Vi desc="成果正在并进正式表、按去向发往下游。落完这一屏会换成收尾结果 —— 不用刷新。" hint="通常几秒钟" /> - ) : detail.card.runtime === 'running' ? ( + ) : detail.card.runtime === 'running' && !holding ? ( /* * **AI 正在算这一拍,不是「已经有一份件摆着」**(2026-08-21 用户: * 「AI 持续推进中时中间的页面不变成持续推进的页面了,而是卡在 html @@ -774,6 +776,14 @@ export function ViewRegion({ detail, onReplyOrder, values, onField, locate }: Vi * 空待处理就被这份**陈旧**的件抢先占住,推进中动画再也显示不出来。 * **正在跑的这一拍,永远比上一拍摆下的旧件优先**:running 判据挪到 * viewHtml 前面,陈旧的件只在「AI 这会儿不在算」时才配拿来顶上。 + * + * **`&& !holding` 是 2026-08-31 补的**(用户:「在中央工作区修改了内容点击 + * 保存后,为什么会出现『AI 正在推进这单』」):上面那段的前提是「viewHtml 是 + * 上一次推的,可能陈旧」。而件上按保存走的 `afterSave` 是**先重推件、再叫醒 + * 任务**(「保存即铺格」)—— 那条路上件比 `running` 还新,顶掉它没有道理; + * 且那一拍按「叫了人就等人」的闸根本不问模型,这一屏写的「看材料、写台账、 + * 调工具都由它来」一件没发生。**有活等着人办就不摆推进场景**,判断收在 + * `awaitingHuman` 里(与服务端 `humanTool.pendingCards` 同一句话)。 */ ) : pending.entry ? ( diff --git a/apps/web/src/features/desk-task/internal/pending-cards.ts b/apps/web/src/features/desk-task/internal/pending-cards.ts index d45cecc..58648c1 100644 --- a/apps/web/src/features/desk-task/internal/pending-cards.ts +++ b/apps/web/src/features/desk-task/internal/pending-cards.ts @@ -23,6 +23,32 @@ export type PendingCards = { readonly entryLedger?: string; }; +/** + * **这一单还有活等着人办吗** —— 未办结的工单,或未回的决策卡 / 录入卡。 + * + * 与服务端 `humanTool.pendingCards`(没办结工单 + 没作答且没被顶替的对话卡)说的是 + * 同一句话,两侧各按自己手上的数据算。 + * + * 用处(2026-08-31 用户:「在中央工作区修改了内容点击保存后,为什么会出现『AI 正在 + * 推进这单』」):件上按保存会走 `afterSave` —— **先重推件、再叫醒任务** + * (「保存即铺格」:人选了芯片类型按保存,配方这才有机会把参数行铺出来;不叫醒 + * 格子永远不出现,2026-08-30 走查在这儿卡死过)。叫醒把 runtime 拨成 `running`, + * 而工作区的 `running` 判据排在 `viewHtml` 前面(2026-08-21:正在跑的这一拍要压过 + * 上一拍摆下的**陈旧**的件)—— 于是人刚填完按下保存,屏上那份**刚重推、含着他刚填 + * 的值**的件被「AI 正在推进这单」顶掉几秒。 + * 那条 2026-08-21 的规则前提是「viewHtml 是上一次推的,可能陈旧」,而这条路上件比 + * `running` 还新,前提不成立;更要紧的是那一拍按「叫了人就等人」的闸 + * (`turn-runner`,2026-08-27)**根本不问模型**,推进场景写着「看材料、写台账、调工具 + * 都由它来」——那些事一件没发生,**界面在撒谎**。 + * + * 所以:**有活等着人办时不摆推进场景**(件 / 录入占位页照旧摆,人正要在上面办事); + * 没有任何卡在等人、AI 真在埋头算下一拍时,推进场景照旧 —— 那正是 2026-08-21 要的。 + * 与服务端那条闸是同一条道理的两侧:**有未答的卡,服务端就不问模型,前端就不说 AI 在推进。** + */ +export function awaitingHuman(input: { readonly openOrders: number; readonly cards: PendingCards }): boolean { + return input.openOrders > 0 || input.cards.decision || input.cards.entry; +} + export function pendingCardsOf(chat: TaskDetail['chat']): PendingCards { let decision = false; let entryLedger: string | undefined;