agent-exec transport a9ccb6178c1cd1ffb0e66f9ad77b6b65/c2d99ab92f51
This commit is contained in:
@@ -9,7 +9,7 @@ import { liveToMessage, livePhaseLabel, runEventsToTimeline, showProcessInline }
|
||||
import { deriveShellPhase, isShellBusy, showLiveBubble, type ShellPhase } from '../runstate'
|
||||
import type { Message, Part, HITLCard, ActivityCard, Artifact } from '../contracts'
|
||||
import { UIRenderer, InlineConfirmCard, hasOpenConfirm, CardAnchor } from '@ui'
|
||||
import { docBridge, pendingConfirmations } from './Workbench'
|
||||
import { docBridge, openPendingConfirmation, pendingConfirmations } from './Workbench'
|
||||
import { splitTimeline, mergeProcessTimeline, artifactIcon, humanSize, artifactIdsIn, markMissingArtifacts, dedupeDeliverables, foldRunErrors, processSummary, humanError } from '../messageparts'
|
||||
import { ProcessTimeline } from './ProcessTimeline'
|
||||
import { ConversationWork } from './ConversationWork'
|
||||
@@ -569,7 +569,6 @@ function ConversationSession() {
|
||||
const bgRunning = runs.filter(r => r.kind === 'subagent' && !isTerminal(r.status)).length
|
||||
const unreadResults = cards.filter(card => card.work?.result && card.work.resultVersion > card.work.readVersion)
|
||||
const pendingHere = pendingConfirmations(cards, currentConvId, currentProjectId)
|
||||
const pendingCardId = cards.find(card => card.conversationId === currentConvId && card.projectId === currentProjectId && card.status === 'action_required')?.id ?? ''
|
||||
|
||||
useEffect(() => {
|
||||
if (!stickRef.current) return
|
||||
@@ -731,12 +730,9 @@ function ConversationSession() {
|
||||
|
||||
<div className="input-bar">
|
||||
<OutboxPanel />
|
||||
{!workPaneOwnsCards && pendingHere > 0 && <button type="button" className="work-result-notice" data-pending-confirm onClick={() => {
|
||||
const body = bodyRef.current
|
||||
const target = (pendingCardId ? body?.querySelector(`[data-card-id="${CSS.escape(pendingCardId)}"]`) : null) ?? body?.querySelector('[data-conversation-work]')
|
||||
target?.scrollIntoView({ block: 'center' })
|
||||
}}>
|
||||
<Icon name="message" size={14} /><span>{pendingHere} 项待你确认</span><span>在对话中处理 ↓</span>
|
||||
{!workPaneOwnsCards && pendingHere > 0 && <button type="button" className="work-result-notice" data-pending-confirm
|
||||
onClick={() => openPendingConfirmation(cards, currentConvId, currentProjectId)}>
|
||||
<Icon name="message" size={14} /><span>{pendingHere} 项待你确认</span><span>在工作栏处理 ↗</span>
|
||||
</button>}
|
||||
{unreadResults.length > 0 && <button type="button" className="work-result-notice" onClick={() => store.openInbox(unreadResults[0].id)}>
|
||||
<Icon name="check" size={14} /><span>{unreadResults.length} 项工作有新结果</span><span>查看工作 ↗</span>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { setPersistedOpen } from '@ui'
|
||||
import type { RdmImpactAnalysis, RdmOperationPage, RdmOperationQuery, RdmOperationRecord } from '../api'
|
||||
import {
|
||||
classifyOperationsError, createOperationsFeed, describeOperation, diffImpact, isLegacyOperation,
|
||||
OperationRow, OperationsListView, startLatestFetch, TraceCoverageView, TreeNodeRow,
|
||||
OperationRow, OperationsListView, startLatestFetch, TraceCoverageView, TreeNodeRow, visibleAuditTabs,
|
||||
type OperationsFeedState,
|
||||
} from './RdmAuditWorkspace'
|
||||
import type { ActivityCard } from '../contracts'
|
||||
import { store } from '../store'
|
||||
import { RDM_AUDIT_COMMANDS, RDM_AUDIT_OPERATIONS_COMMANDS, rdmAuditOperationsReady } from '../workspaces'
|
||||
import { BasisStrip } from './Conversation'
|
||||
import { candidateActions, candidateChanges } from './ConversationWork'
|
||||
import { pendingConfirmations } from './Workbench'
|
||||
import { openPendingConfirmation, pendingConfirmations } from './Workbench'
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (v: T) => void
|
||||
@@ -310,6 +313,58 @@ describe('OperationRow 展开内容', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('证据与影响分析折叠(@ui Collapsible,稳定 id)', () => {
|
||||
const row = () => renderToStaticMarkup(<OperationRow pid="p1" record={record('a')} defaultOpen />)
|
||||
const trigger = (expanded: boolean, text: string) =>
|
||||
new RegExp(`<button type="button" class="mk-col-trigger\\s*" aria-expanded="${expanded}"><span class="rdm-audit-err-toggle rdm-audit-wrap">${text}</span></button>`)
|
||||
|
||||
it('默认收起:触发钮由 Collapsible 渲染,详情未挂载(不发起读取)', () => {
|
||||
const html = row()
|
||||
expect(html).toMatch(trigger(false, '查看证据:笔记切片 SL-9'))
|
||||
expect(html).toMatch(trigger(false, '查看影响分析 IA-1'))
|
||||
expect(html).not.toContain('btn v-ghost rdm-audit-wrap')
|
||||
expect(html).not.toContain('加载中…')
|
||||
})
|
||||
|
||||
it('展开态按稳定 id 记忆:重挂载后恢复展开并挂载详情', () => {
|
||||
setPersistedOpen('rdm-audit-evidence:p1/slice/SL-9', true)
|
||||
setPersistedOpen('rdm-audit-impact:p1/IA-1', true)
|
||||
try {
|
||||
const html = row()
|
||||
expect(html).toMatch(trigger(true, '查看证据:笔记切片 SL-9'))
|
||||
expect(html).toMatch(trigger(true, '查看影响分析 IA-1'))
|
||||
expect(html.match(/加载中…/g)).toHaveLength(2)
|
||||
} finally {
|
||||
setPersistedOpen('rdm-audit-evidence:p1/slice/SL-9', false)
|
||||
setPersistedOpen('rdm-audit-impact:p1/IA-1', false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('审计记录页签能力门禁', () => {
|
||||
const enabled = (ids: readonly string[]) => ids.map(id => ({ id, enabled: true }))
|
||||
|
||||
it('五条 W8b/W5 命令齐备且启用才就绪;缺一、停用或清单不可用均不就绪', () => {
|
||||
expect(RDM_AUDIT_OPERATIONS_COMMANDS).toHaveLength(5)
|
||||
const all = enabled(RDM_AUDIT_OPERATIONS_COMMANDS)
|
||||
expect(rdmAuditOperationsReady(all)).toBe(true)
|
||||
for (const missing of RDM_AUDIT_OPERATIONS_COMMANDS) {
|
||||
expect(rdmAuditOperationsReady(all.filter(c => c.id !== missing))).toBe(false)
|
||||
expect(rdmAuditOperationsReady(all.map(c => (c.id === missing ? { ...c, enabled: false } : c)))).toBe(false)
|
||||
}
|
||||
expect(rdmAuditOperationsReady(null)).toBe(false)
|
||||
})
|
||||
|
||||
it('只有 rdm.inspect.* 七命令的构建上不就绪', () => {
|
||||
expect(rdmAuditOperationsReady(enabled(RDM_AUDIT_COMMANDS))).toBe(false)
|
||||
})
|
||||
|
||||
it('能力缺失时不暴露「审计记录」页签,其余页签不受影响', () => {
|
||||
expect(visibleAuditTabs(false).map(t => t.label)).toEqual(['概览', '资料与文档', '关联追溯', '操作记录'])
|
||||
expect(visibleAuditTabs(true).map(t => t.label)).toEqual(['概览', '资料与文档', '关联追溯', '操作记录', '审计记录'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('TraceCoverageView 部分覆盖', () => {
|
||||
it('候选不计正式覆盖;缺项与待复核提示部分覆盖', () => {
|
||||
const html = renderToStaticMarkup(<TraceCoverageView pid="p1" data={{
|
||||
@@ -380,6 +435,26 @@ describe('对话侧:候选差异、确认路径与依据条', () => {
|
||||
expect(pendingConfirmations(cards, 'v1', 'p2')).toBe(0)
|
||||
})
|
||||
|
||||
it('右栏收起后的确认路径经 openInbox 定位首张待处理卡,与计数同一谓词(含决议开放的工作卡)', () => {
|
||||
const openInbox = vi.spyOn(store, 'openInbox').mockImplementation(() => {})
|
||||
try {
|
||||
const decisionOpen = { resultVersion: 1, readVersion: 1, decision: { mode: 'single' as const, state: 'open' as const } }
|
||||
const cards = [
|
||||
card({ id: 'dismissed', status: 'dismissed' }),
|
||||
card({ id: 'elsewhere', conversationId: 'other' }),
|
||||
card({ id: 'decision', status: 'active', work: decisionOpen }),
|
||||
card({ id: 'confirm' }),
|
||||
]
|
||||
expect(pendingConfirmations(cards, 'v1', 'p1')).toBe(2)
|
||||
openPendingConfirmation(cards, 'v1', 'p1')
|
||||
expect(openInbox).toHaveBeenLastCalledWith('decision')
|
||||
openPendingConfirmation(cards.filter(c => c.id !== 'decision'), 'v1', 'p1')
|
||||
expect(openInbox).toHaveBeenLastCalledWith('confirm')
|
||||
} finally {
|
||||
openInbox.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('依据条列出来源与 token 摘要,不含正文', () => {
|
||||
const html = renderToStaticMarkup(<BasisStrip obs={{
|
||||
items: ['项目知识', '对话历史'], estChars: 1800, truncations: ['历史已裁剪'],
|
||||
|
||||
@@ -53,6 +53,11 @@ const TABS = [
|
||||
] as const
|
||||
type TabId = (typeof TABS)[number]['id']
|
||||
|
||||
/** 「审计记录」页签及其覆盖面板、影响分析入口只在 W8b/W5 命令面就绪时暴露(store.rdmAuditOperationsAvailable)。 */
|
||||
export function visibleAuditTabs(operationsAvailable: boolean) {
|
||||
return TABS.filter(t => t.id !== 'operations' || operationsAvailable)
|
||||
}
|
||||
|
||||
const OPERATION_FILTERS = [
|
||||
['', '全部操作'],
|
||||
['doc.instantiate', '创建文档实例'],
|
||||
@@ -107,6 +112,8 @@ export function RdmAuditWorkspace() {
|
||||
|
||||
const project = caps?.projects.find(p => p.projectId === pid)
|
||||
const exportOn = useStore(s => s.rdmExportAvailable)
|
||||
const tabs = visibleAuditTabs(useStore(s => s.rdmAuditOperationsAvailable))
|
||||
const activeTab: TabId = tabs.some(t => t.id === tab) ? tab : 'summary'
|
||||
if (error) return <RdmErrorState raw={error} />
|
||||
if (!caps || capsIdentity !== identity) return <div className="empty-note">加载中…</div>
|
||||
if (caps.projects.length === 0) return <EmptyState icon="folder">无可见研发项目——项目创建者与域管理员可查看追溯审计</EmptyState>
|
||||
@@ -123,18 +130,18 @@ export function RdmAuditWorkspace() {
|
||||
{caps.crossProject && <Chip tone="info">跨项目</Chip>}
|
||||
</div>
|
||||
<div className="tabs rdm-audit-tabs" role="tablist">
|
||||
{TABS.map(t => (
|
||||
<button key={t.id} role="tab" aria-selected={tab === t.id}
|
||||
className={`tab${tab === t.id ? ' active' : ''}`}
|
||||
{tabs.map(t => (
|
||||
<button key={t.id} role="tab" aria-selected={activeTab === t.id}
|
||||
className={`tab${activeTab === t.id ? ' active' : ''}`}
|
||||
onClick={() => setTab(t.id)}>{t.label}</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="rdm-audit-body">
|
||||
{tab === 'summary' && <SummaryTab pid={pid} exportOn={exportOn} />}
|
||||
{tab === 'materials' && <MaterialsTab key={pid} pid={pid} />}
|
||||
{tab === 'trace' && <TraceTab pid={pid} />}
|
||||
{tab === 'timeline' && <TimelineTab pid={pid} />}
|
||||
{tab === 'operations' && <OperationsTab key={pid} pid={pid} />}
|
||||
{activeTab === 'summary' && <SummaryTab pid={pid} exportOn={exportOn} />}
|
||||
{activeTab === 'materials' && <MaterialsTab key={pid} pid={pid} />}
|
||||
{activeTab === 'trace' && <TraceTab pid={pid} />}
|
||||
{activeTab === 'timeline' && <TimelineTab pid={pid} />}
|
||||
{activeTab === 'operations' && <OperationsTab key={pid} pid={pid} />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -1181,32 +1188,33 @@ function EvidenceList({ pid, refs }: { pid: string; refs: RdmOperationRefs }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** 证据跳转:按需读取对象详情;服务端按当前正文权判定,无权时只给出可读拒绝,不假设有正文。 */
|
||||
/** 证据跳转:展开时才读取对象详情;服务端按当前正文权判定,无权时只给出可读拒绝,不假设有正文。 */
|
||||
function EvidenceLink({ pid, evidence }: { pid: string; evidence: RdmOperationRef }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const label = `${evidence.kind.label} ${evidence.id}`
|
||||
return (
|
||||
<Collapsible id={`rdm-audit-evidence:${pid}/${evidence.kind.key}/${evidence.id}`}
|
||||
trigger={<span className="rdm-audit-err-toggle rdm-audit-wrap">{`查看证据:${label}`}</span>}>
|
||||
<EvidenceDetail pid={pid} evidence={evidence} label={label} />
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
|
||||
function EvidenceDetail({ pid, evidence, label }: { pid: string; evidence: RdmOperationRef; label: string }) {
|
||||
const [detail, setDetail] = useState<ObjectDetail | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
const genRef = useRef(0)
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
setDetail(null); setErr('')
|
||||
return startLatestFetch(genRef,
|
||||
() => api.rdmCommand<ObjectDetail>('rdm.inspect.object', { projectId: pid, kind: evidence.kind.key, id: evidence.id }),
|
||||
setDetail, setErr)
|
||||
}, [open, pid, evidence.kind.key, evidence.id])
|
||||
const label = `${evidence.kind.label} ${evidence.id}`
|
||||
return (
|
||||
<div>
|
||||
<button type="button" className="btn v-ghost rdm-audit-wrap" aria-expanded={open} onClick={() => setOpen(!open)}>
|
||||
{open ? `收起证据:${label}` : `查看证据:${label}`}
|
||||
</button>
|
||||
{open && (err
|
||||
? (/权限拒绝|无权|受限/.test(err)
|
||||
? <div className="rdm-audit-truncated">无权查看该证据(跨项目、无权或已撤权),仅保留元数据:{label}</div>
|
||||
: <RdmErrorState raw={err} />)
|
||||
: !detail ? <div className="empty-note">加载中…</div> : <ObjectDetailView detail={detail} />)}
|
||||
</div>
|
||||
)
|
||||
}, [pid, evidence.kind.key, evidence.id])
|
||||
if (err) {
|
||||
return /权限拒绝|无权|受限/.test(err)
|
||||
? <div className="rdm-audit-truncated">无权查看该证据(跨项目、无权或已撤权),仅保留元数据:{label}</div>
|
||||
: <RdmErrorState raw={err} />
|
||||
}
|
||||
return !detail ? <div className="empty-note">加载中…</div> : <ObjectDetailView detail={detail} />
|
||||
}
|
||||
|
||||
const refKey = (r: RdmObjectRef) => `${r.kind}:${r.id}`
|
||||
@@ -1234,9 +1242,19 @@ function ImpactSnapshotView({ title, a }: { title: string; a: RdmImpactAnalysis
|
||||
)
|
||||
}
|
||||
|
||||
/** 当时与当前影响:读取不可变快照;重算须显式触发(生成新分析快照,不覆盖历史)。 */
|
||||
/** 当时与当前影响:展开时才读取不可变快照;重算须显式触发(生成新分析快照,不覆盖历史)。 */
|
||||
function ImpactAnalysisBlock({ pid, analysisId }: { pid: string; analysisId: string }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<div className="rdm-audit-detail-body">
|
||||
<Collapsible id={`rdm-audit-impact:${pid}/${analysisId}`}
|
||||
trigger={<span className="rdm-audit-err-toggle rdm-audit-wrap">{`查看影响分析 ${analysisId}`}</span>}>
|
||||
<ImpactAnalysisDetail pid={pid} analysisId={analysisId} />
|
||||
</Collapsible>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ImpactAnalysisDetail({ pid, analysisId }: { pid: string; analysisId: string }) {
|
||||
const [snapshot, setSnapshot] = useState<RdmImpactAnalysis | null>(null)
|
||||
const [current, setCurrent] = useState<RdmImpactAnalysis | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
@@ -1244,10 +1262,9 @@ function ImpactAnalysisBlock({ pid, analysisId }: { pid: string; analysisId: str
|
||||
const snapRef = useRef(0)
|
||||
const currentRef = useRef(0)
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
setSnapshot(null); setErr('')
|
||||
return startLatestFetch(snapRef, () => api.rdmImpactGet(pid, analysisId), setSnapshot, setErr)
|
||||
}, [open, pid, analysisId])
|
||||
}, [pid, analysisId])
|
||||
useEffect(() => () => { currentRef.current++ }, [pid, analysisId])
|
||||
|
||||
const revision = Number(snapshot?.start.revision)
|
||||
@@ -1262,30 +1279,23 @@ function ImpactAnalysisBlock({ pid, analysisId }: { pid: string; analysisId: str
|
||||
const diff = snapshot && current ? diffImpact(snapshot, current) : null
|
||||
|
||||
return (
|
||||
<div className="rdm-audit-detail-body">
|
||||
<button type="button" className="btn v-ghost rdm-audit-wrap" aria-expanded={open} onClick={() => setOpen(!open)}>
|
||||
{open ? '收起影响分析' : `查看影响分析 ${analysisId}`}
|
||||
</button>
|
||||
{open && (
|
||||
<>
|
||||
{err && <RdmErrorState raw={err} />}
|
||||
{!snapshot && !err && <div className="empty-note">加载中…</div>}
|
||||
{snapshot && <ImpactSnapshotView title="当时影响(不可变快照)" a={snapshot} />}
|
||||
{snapshot && revision > 0 && (
|
||||
<button type="button" className="btn v-ghost" disabled={busy} onClick={recompute}>
|
||||
{busy ? '正在重算…' : '按当前受控图重算(生成新快照,不覆盖历史)'}
|
||||
</button>
|
||||
)}
|
||||
{current && <ImpactSnapshotView title="当前影响" a={current} />}
|
||||
{diff && (
|
||||
<div className="rdm-audit-detail-body rdm-audit-wrap">
|
||||
<div>当前新增:{diff.added.length ? diff.added.map(refLabel).join('、') : '无'}</div>
|
||||
<div>当前不再涉及:{diff.removed.length ? diff.removed.map(refLabel).join('、') : '无'}</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<>
|
||||
{err && <RdmErrorState raw={err} />}
|
||||
{!snapshot && !err && <div className="empty-note">加载中…</div>}
|
||||
{snapshot && <ImpactSnapshotView title="当时影响(不可变快照)" a={snapshot} />}
|
||||
{snapshot && revision > 0 && (
|
||||
<button type="button" className="btn v-ghost" disabled={busy} onClick={recompute}>
|
||||
{busy ? '正在重算…' : '按当前受控图重算(生成新快照,不覆盖历史)'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{current && <ImpactSnapshotView title="当前影响" a={current} />}
|
||||
{diff && (
|
||||
<div className="rdm-audit-detail-body rdm-audit-wrap">
|
||||
<div>当前新增:{diff.added.length ? diff.added.map(refLabel).join('、') : '无'}</div>
|
||||
<div>当前不再涉及:{diff.removed.length ? diff.removed.map(refLabel).join('、') : '无'}</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -34,10 +34,21 @@ const scratchInboxCallbacks = {
|
||||
}
|
||||
const inboxCallbacks = { ...scratchInboxCallbacks, onArchive: (c: ActivityCard) => store.cardToProject(c) }
|
||||
|
||||
/** 本会话仍待用户处理的卡片数:右栏收起后由对话承接确认路径。 */
|
||||
export function pendingConfirmations(cards: ActivityCard[], conversationId: string | null | undefined, projectId: string | null | undefined): number {
|
||||
return cards.filter(card => card.status !== 'dismissed' && card.conversationId === conversationId && card.projectId === projectId
|
||||
&& (card.status === 'action_required' || card.work?.decision?.state === 'open')).length
|
||||
type Scope = string | null | undefined
|
||||
|
||||
/** 本会话仍待用户处理的卡片:计数与定位共用这一谓词。 */
|
||||
const isPendingConfirmation = (card: ActivityCard, conversationId: Scope, projectId: Scope) =>
|
||||
card.status !== 'dismissed' && card.conversationId === conversationId && card.projectId === projectId
|
||||
&& (card.status === 'action_required' || card.work?.decision?.state === 'open')
|
||||
|
||||
/** 本会话仍待用户处理的卡片数:右栏收起后由对话输入区提示承接确认路径。 */
|
||||
export function pendingConfirmations(cards: ActivityCard[], conversationId: Scope, projectId: Scope): number {
|
||||
return cards.filter(card => isPendingConfirmation(card, conversationId, projectId)).length
|
||||
}
|
||||
|
||||
/** 右栏收起或不在「工作」页签时的确认路径:经 openInbox 展开右栏、切到工作页签并定位首张待处理卡。 */
|
||||
export function openPendingConfirmation(cards: ActivityCard[], conversationId: Scope, projectId: Scope) {
|
||||
store.openInbox(cards.find(card => isPendingConfirmation(card, conversationId, projectId))?.id)
|
||||
}
|
||||
|
||||
/** 项目收件箱视图(CF3/D49/D51):项目级卡片 ∪ 其他会话待办(done 不聚合)。 */
|
||||
@@ -79,7 +90,7 @@ export function Workbench() {
|
||||
const pending = pendingCount(visible)
|
||||
const unread = visible.filter(card => card.work && card.work.resultVersion > card.work.readVersion).length
|
||||
const pendingHere = useStore(s => pendingConfirmations(s.cards, s.currentConvId, s.currentProjectId))
|
||||
const collapseHint = pendingHere > 0 ? `收起右栏(${pendingHere} 项待确认会在对话中继续处理)` : '收起右栏'
|
||||
const collapseHint = pendingHere > 0 ? `收起右栏(${pendingHere} 项待确认可从对话输入区的提示回到工作栏处理)` : '收起右栏'
|
||||
// 离开文件入口时收回全屏展开,避免出现「工作」铺满主区的怪态。
|
||||
const switchTab = (tab: 'work' | 'files') =>
|
||||
store.setUI({ rightTab: tab, previewExpanded: tab === 'files' ? store.getState().ui.previewExpanded : false })
|
||||
|
||||
+9
-3
@@ -11,7 +11,7 @@ import { canFlushOutbox, deriveShellPhase } from './runstate'
|
||||
import { flashElement } from './scrollsync'
|
||||
import { isThemeChoice, resolveTheme, type ThemeChoice } from './theme'
|
||||
import { mergeSurfacesByRevision, surfaceKey } from './surfaceKey'
|
||||
import { cockpitReady, rdmAuditReady, rdmExportReady } from './workspaces'
|
||||
import { cockpitReady, rdmAuditOperationsReady, rdmAuditReady, rdmExportReady } from './workspaces'
|
||||
import { rdmRolesReady, rdmMembersReady, rdmDutyReady, rdmPermMatrixReady } from './rdmManagement'
|
||||
import { NO_RENDER_CAPABILITIES, renderCapabilities, type RenderCapabilities } from './documentRenderPresentation'
|
||||
|
||||
@@ -148,6 +148,8 @@ interface AppState {
|
||||
rdmCockpitAvailable: boolean
|
||||
/** 概览导出区:三条 rdm.inspect.export.* 都启用才为 true。不参与七命令就绪门。 */
|
||||
rdmExportAvailable: boolean
|
||||
/** 「审计记录」页签(W10):五条 W8b/W5 命令都启用才为 true。不参与七命令就绪门。 */
|
||||
rdmAuditOperationsAvailable: boolean
|
||||
/** U6 RDM 管理页能力:命令面齐备才显示对应分区/页签(普通基座零暴露,fail-closed)。 */
|
||||
rdmRolesAvailable: boolean
|
||||
rdmMembersAvailable: boolean
|
||||
@@ -227,6 +229,7 @@ let state: AppState = {
|
||||
rdmAuditAvailable: false,
|
||||
rdmCockpitAvailable: false,
|
||||
rdmExportAvailable: false,
|
||||
rdmAuditOperationsAvailable: false,
|
||||
rdmRolesAvailable: false, rdmMembersAvailable: false, rdmDocReviewAvailable: false, rdmPermMatrixAvailable: false,
|
||||
rdmRender: NO_RENDER_CAPABILITIES,
|
||||
adminSections: [],
|
||||
@@ -659,7 +662,8 @@ export const store = {
|
||||
plan: null, todos: null, artifacts: [], surfaces: [], projectSurfaces: [],
|
||||
cards: [], projectCards: [], cardActionStates: {}, workRecord: null, workReference: null, inboxScope: 'conv',
|
||||
runtimeMock: false, profiles: [], pluginPanels: [], serverWorkspaces: [], adminSections: [],
|
||||
rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, rdmRolesAvailable: false, rdmMembersAvailable: false,
|
||||
rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, rdmAuditOperationsAvailable: false,
|
||||
rdmRolesAvailable: false, rdmMembersAvailable: false,
|
||||
rdmDocReviewAvailable: false, rdmPermMatrixAvailable: false, rdmRender: NO_RENDER_CAPABILITIES,
|
||||
connected: false, posting: false, error: null, toast: null, outbox: [], outboxDraining: false,
|
||||
previewTabs: {}, previewActive: {}, previewOpenIntent: 0 })
|
||||
@@ -1466,6 +1470,7 @@ export const store = {
|
||||
rdmAuditAvailable: rdmAuditReady(cmds),
|
||||
rdmCockpitAvailable: cockpitReady(cmds),
|
||||
rdmExportAvailable: rdmExportReady(cmds),
|
||||
rdmAuditOperationsAvailable: rdmAuditOperationsReady(cmds),
|
||||
rdmRolesAvailable: rdmRolesReady(cmds),
|
||||
rdmMembersAvailable: rdmMembersReady(cmds),
|
||||
rdmDocReviewAvailable: rdmDutyReady(cmds),
|
||||
@@ -1474,7 +1479,8 @@ export const store = {
|
||||
})
|
||||
} catch {
|
||||
if (epoch !== identityEpoch) return
|
||||
set({ rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, rdmRolesAvailable: false, rdmMembersAvailable: false,
|
||||
set({ rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, rdmAuditOperationsAvailable: false,
|
||||
rdmRolesAvailable: false, rdmMembersAvailable: false,
|
||||
rdmDocReviewAvailable: false, rdmPermMatrixAvailable: false, rdmRender: NO_RENDER_CAPABILITIES })
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -54,6 +54,21 @@ export function rdmAuditReady(cmds: { id?: string; enabled?: boolean }[] | null
|
||||
return RDM_AUDIT_COMMANDS.every(need => cmds.some(c => c?.id === need && c.enabled === true))
|
||||
}
|
||||
|
||||
/** 「审计记录」页签(W10)消费的 W8b/W5 命令:操作记录列表、受控覆盖面板、影响分析快照与重算。不并入七命令就绪门。 */
|
||||
export const RDM_AUDIT_OPERATIONS_COMMANDS = [
|
||||
'rdm.audit.operations',
|
||||
'rdm.trace.coverage',
|
||||
'rdm.trace.pending-recheck',
|
||||
'rdm.compliance.impact.get',
|
||||
'rdm.compliance.impact.recompute',
|
||||
] as const
|
||||
|
||||
/** 五命令齐备且 enabled 才展示「审计记录」页签(能力缺失即不暴露,避免进入即 400)。 */
|
||||
export function rdmAuditOperationsReady(cmds: { id?: string; enabled?: boolean }[] | null | undefined): boolean {
|
||||
if (!Array.isArray(cmds)) return false
|
||||
return RDM_AUDIT_OPERATIONS_COMMANDS.every(need => cmds.some(c => c?.id === need && c.enabled === true))
|
||||
}
|
||||
|
||||
/** 文档驾驶舱消费的全部 rdm.cockpit.* 命令(I1 只读面;rollout I2)。 */
|
||||
export const COCKPIT_COMMANDS = [
|
||||
'rdm.cockpit.overview',
|
||||
|
||||
Reference in New Issue
Block a user