diff --git a/web/src/App.tsx b/web/src/App.tsx index 845fb1a5..a6adf0e0 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -59,6 +59,10 @@ import { resolveReconnectRunId, shouldSubscribeExistingRun, } from './lib/taskRunReconnect'; +import { + GOVERNANCE_BLOCKED_HINT, + isBlockedGovernanceStatus, +} from './lib/blockedGovernanceCopy'; import { buildControlledLoadErrorSkeleton, buildControlledLoadingSkeleton, @@ -2815,6 +2819,19 @@ export default function App() { retainedRunId = created.run_id; taskRunStream.setRunId(sessionId, created.run_id); patchAsst({ taskRunId: created.run_id }); + if (isBlockedGovernanceStatus(created.status)) { + settled = true; + patchAsst({ + status: 'blocked_pending_governance', + content: GOVERNANCE_BLOCKED_HINT, + livePhaseLabel: undefined, + taskRunId: created.run_id, + }); + taskRunStream.setRunId(sessionId, null); + taskRunStream.setThinking(sessionId, false); + await chatWindow.refresh().catch(() => undefined); + return; + } await subscribeTaskRunEvents(created.run_id, { onEvent }, controller.signal); return; } @@ -2844,6 +2861,18 @@ export default function App() { taskRunStream.setThinking(sessionId, false); return; } + if (isBlockedGovernanceStatus(remote.status)) { + settled = true; + patchAsst({ + status: 'blocked_pending_governance', + content: GOVERNANCE_BLOCKED_HINT, + livePhaseLabel: undefined, + taskWorkspaceId: remote.workspace_id, + taskRunId: runId, + }); + taskRunStream.setThinking(sessionId, false); + return; + } if (remote.status === 'completed') { settled = true; patchAsst({ diff --git a/web/src/api/chatWindow.ts b/web/src/api/chatWindow.ts index aae11add..88dd6964 100644 --- a/web/src/api/chatWindow.ts +++ b/web/src/api/chatWindow.ts @@ -28,6 +28,8 @@ export function mapTaskStatusToMessageStatus( case 'quality_failed': // 门禁未过必须保持失败态,不得映射 completed return 'quality_failed'; + case 'blocked_pending_governance': + return 'blocked_pending_governance'; case 'completed': case '': return 'completed'; @@ -47,6 +49,8 @@ export function assistantTaskStatusLabel(status: AssistantMessageStatus | undefi return '待编制人确认'; case 'quality_failed': return '质量未通过'; + case 'blocked_pending_governance': + return '待激活工作流'; case 'failed': return '执行失败'; case 'cancelled': diff --git a/web/src/components/chat/MessageBubble.tsx b/web/src/components/chat/MessageBubble.tsx index 4ff483a4..6a3961b4 100644 --- a/web/src/components/chat/MessageBubble.tsx +++ b/web/src/components/chat/MessageBubble.tsx @@ -1,6 +1,7 @@ import React, { useMemo, useState } from 'react'; import { ChatErrorInfo, ChatMessage } from '../../types/workbench'; import { assistantTaskStatusLabel } from '../../api/chatWindow'; +import { isBlockedGovernanceBubble } from '../../lib/blockedGovernanceCopy'; import { PhaseEventStrip } from './PhaseEventStrip'; import { RecommendedCaseCard } from './RecommendedCaseCard'; import { InteractiveCard } from './InteractiveCard'; @@ -62,6 +63,9 @@ const MessageBubbleInner: React.FC = ({ const isFailed = !isUser && status === 'failed' && !!message.errorInfo; const isCancelled = !isUser && status === 'cancelled'; const isPendingAuthorConfirmation = !isUser && status === 'pending_author_confirmation'; + const isBlockedGovernance = + !isUser && + isBlockedGovernanceBubble({ taskStatus: status, content: message.content }); const taskStatusLabel = assistantTaskStatusLabel(status); const hasContent = !!message.content && message.content.trim().length > 0; const [diagCopied, setDiagCopied] = useState(false); @@ -273,7 +277,7 @@ const MessageBubbleInner: React.FC = ({ ? 'var(--brand-500)' : isFailed ? 'var(--danger-bg)' - : isPendingAuthorConfirmation + : isPendingAuthorConfirmation || isBlockedGovernance ? 'rgba(245, 158, 11, 0.08)' : 'var(--bg-panel)', color: isUser ? '#ffffff' : 'var(--text-strong)', @@ -281,7 +285,7 @@ const MessageBubbleInner: React.FC = ({ ? 'none' : isFailed ? '1px solid var(--danger)' - : isPendingAuthorConfirmation + : isPendingAuthorConfirmation || isBlockedGovernance ? '1px solid rgba(245, 158, 11, 0.45)' : '1px solid var(--border)', boxShadow: isUser ? '0 2px 8px rgba(79, 107, 255, 0.25)' : 'var(--shadow-xs)', diff --git a/web/src/components/doc/DocGovActionBar.tsx b/web/src/components/doc/DocGovActionBar.tsx index 856800d8..e0126d21 100644 --- a/web/src/components/doc/DocGovActionBar.tsx +++ b/web/src/components/doc/DocGovActionBar.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import { DocumentAST } from '../../types/block'; import { UserRole } from '../../types/docgov'; +import { canShowDocGovApprovalAction, isUnmanagedDocDuties } from '../../lib/docGovActionVisibility'; import { Send, CheckCircle2, @@ -68,6 +69,11 @@ export const DocGovActionBar: React.FC = ({ const isPrivate = ast.category === 'private'; const isShared = ast.category === 'shared'; const isWorkflow = !isPrivate && !isShared; + const duties = { authorId: ast.authorId, reviewerId: ast.reviewerId, approverId: ast.approverId }; + const unmanagedDuties = isUnmanagedDocDuties(duties); + const showSubmit = canShowDocGovApprovalAction('submit', currentRole, duties); + const showReview = canShowDocGovApprovalAction('review', currentRole, duties); + const showApprove = canShowDocGovApprovalAction('approve', currentRole, duties); return (
= ({ {/* 1. 编制阶段 */} {ast.lifecycleState === 'drafting' && ( <> - {currentRole === 'author' ? ( + {unmanagedDuties ? ( + + 该文档未纳入人工审批,不提供送审、审核或批准 + + ) : showSubmit ? ( <> {!ast.isClaimed ? (

- 确认后将锁定工作流配置,运行期间不可直接修改开关与职责。令牌有效期至{' '} - {new Date(preview.expires_at).toLocaleTimeString()}。 + 确认后将锁定工作流配置,运行期间不可直接修改开关与职责。{ACTIVATION_UNMANAGED_FREEZE_HINT}{' '} + 令牌有效期至 {new Date(preview.expires_at).toLocaleTimeString()}。

)} diff --git a/web/src/lib/squadCompletionObservation.ts b/web/src/lib/squadCompletionObservation.ts index 8113bbdc..3c8c1a06 100644 --- a/web/src/lib/squadCompletionObservation.ts +++ b/web/src/lib/squadCompletionObservation.ts @@ -1,12 +1,17 @@ export const INFLIGHT_CHAT_STATUSES = ['pending', 'streaming_status', 'auto_retrying'] as const; export const SUCCESS_TERMINAL_CHAT_STATUSES = ['completed', 'pending_author_confirmation'] as const; -export const ERROR_TERMINAL_CHAT_STATUSES = ['failed', 'cancelled', 'quality_failed'] as const; +export const ERROR_TERMINAL_CHAT_STATUSES = [ + 'failed', + 'cancelled', + 'quality_failed', + 'blocked_pending_governance', +] as const; export const SUCCESS_TERMINAL_STATUS_RE = /^(completed|pending_author_confirmation)$/; -export const ERROR_TERMINAL_STATUS_RE = /^(failed|cancelled|quality_failed)$/; +export const ERROR_TERMINAL_STATUS_RE = /^(failed|cancelled|quality_failed|blocked_pending_governance)$/; export const ANY_TERMINAL_STATUS_RE = - /^(completed|pending_author_confirmation|failed|cancelled|quality_failed)$/; + /^(completed|pending_author_confirmation|failed|cancelled|quality_failed|blocked_pending_governance)$/; export type SquadObservationFrame = { userPromptMatched: boolean; diff --git a/web/src/lib/taskRunReconnect.ts b/web/src/lib/taskRunReconnect.ts index 237d28e5..8776adc6 100644 --- a/web/src/lib/taskRunReconnect.ts +++ b/web/src/lib/taskRunReconnect.ts @@ -21,7 +21,8 @@ export function isRemoteTaskTerminal(status: string | null | undefined): boolean return ( status === 'completed' || status === 'pending_author_confirmation' || - status === 'quality_failed' + status === 'quality_failed' || + status === 'blocked_pending_governance' ); } diff --git a/web/src/types/workbench.ts b/web/src/types/workbench.ts index 077312bb..84f5464a 100644 --- a/web/src/types/workbench.ts +++ b/web/src/types/workbench.ts @@ -117,6 +117,7 @@ export type AssistantMessageStatus = | 'pending_author_confirmation' | 'failed' | 'quality_failed' + | 'blocked_pending_governance' | 'cancelled'; export interface ChatErrorInfo {