// 体系工具排位:服务端声明的工作区对所有用户可见,按 shortcut 数字占位; // 内置「代理」补进空位。文件浏览在第三栏「文件」入口(方案:入口收敛),不再占体系工具位。 // 插件管理面板不进这条清单。 // 2026-10-12 增量 2(审计入口能力门控):「追溯审计」不再无条件内置——普通基座 // 没有任何 rdm.inspect.* 命令,入口可达而进入即 400。仅当命令面探测通过 //(rdmAuditReady / store.rdmAuditAvailable)才纳入;代理仍无条件以 ⌘1 补位。 export interface WorkspaceEntry { key: string title: string icon: string shortcut: string } export interface WorkspaceSource { id: string title: string icon?: string shortcut?: string } /** 审计工作区消费的全部 rdm.inspect.* 命令(capabilities 入口 + 四页签取数)。 */ export const RDM_AUDIT_COMMANDS = [ 'rdm.inspect.capabilities', 'rdm.inspect.summary', 'rdm.inspect.tree', 'rdm.inspect.object', 'rdm.inspect.relations', 'rdm.inspect.events', 'rdm.inspect.history', ] as const /** 概览页导出区单独探测。不并入上面的七命令就绪门。 */ export const RDM_EXPORT_COMMANDS = [ 'rdm.inspect.export.create', 'rdm.inspect.export.status', 'rdm.inspect.export.download', ] as const /** 三条导出命令都启用才显示概览导出区。缺一或清单不可用则隐藏。 */ export function rdmExportReady(cmds: { id?: string; enabled?: boolean }[] | null | undefined): boolean { if (!Array.isArray(cmds)) return false return RDM_EXPORT_COMMANDS.every(need => cmds.some(c => c?.id === need && c.enabled === true)) } /** 正式报告被完整性门拒绝时,界面先确认再带 draft 重试。 */ export function exportNeedsDraft(message: string): boolean { return /不完整|草稿/.test(message) } /** GET /api/commands 清单是否足以支撑审计工作区:七命令齐备且 enabled。清单不可用(未登录/请求失败)视为未就绪。 */ export function rdmAuditReady(cmds: { id?: string; enabled?: boolean }[] | null | undefined): boolean { if (!Array.isArray(cmds)) return false return RDM_AUDIT_COMMANDS.every(need => cmds.some(c => c?.id === need && c.enabled === true)) } /** 文档驾驶舱消费的全部 rdm.cockpit.* 命令(I1 只读面;rollout I2)。 */ export const COCKPIT_COMMANDS = [ 'rdm.cockpit.overview', 'rdm.cockpit.tree', 'rdm.cockpit.node', 'rdm.cockpit.node.sources', 'rdm.cockpit.node.history', 'rdm.cockpit.node.impacts', 'rdm.cockpit.node.conflicts', ] as const /** 七命令齐备且 enabled 才展示驾驶舱入口(与 rdmAuditReady 同源语义)。 */ export function cockpitReady(cmds: { id?: string; enabled?: boolean }[] | null | undefined): boolean { if (!Array.isArray(cmds)) return false return COCKPIT_COMMANDS.every(need => cmds.some(c => c?.id === need && c.enabled === true)) } const BUILTINS: Omit[] = [ { key: 'agents', title: '代理', icon: '🤖' }, { key: 'rdm-audit', title: '追溯审计', icon: '🔍' }, { key: 'rdm-cockpit', title: '文档驾驶舱', icon: '📋' }, ] function shortcutNum(raw?: string): number | null { if (!raw) return null const m = raw.match(/(\d)/) if (!m) return null const n = Number(m[1]) return n >= 1 && n <= 9 ? n : null } interface BuildWorkspaceOptions { /** rdm.inspect.* 命令面探测通过(store.rdmAuditAvailable)。默认 false:能力未知/缺失不展示审计入口。 */ rdmAudit?: boolean /** rdm.cockpit.* 命令面探测通过(store.rdmCockpitAvailable)。默认 false:能力未知/缺失不展示驾驶舱入口。 */ rdmCockpit?: boolean } export function buildWorkspaceEntries(server: WorkspaceSource[], opts: BuildWorkspaceOptions = {}): WorkspaceEntry[] { const used = new Set() const ranked: { rank: number; entry: Omit }[] = [] const unranked: Omit[] = [] for (const w of server) { const entry = { key: `ws:${w.id}`, title: w.title, icon: w.icon || '🗂' } const n = shortcutNum(w.shortcut) if (n && !used.has(n)) { used.add(n) ranked.push({ rank: n, entry }) } else { unranked.push(entry) } } const takeFree = (prefer: number): number => { if (!used.has(prefer) && prefer >= 1 && prefer <= 9) { used.add(prefer) return prefer } for (let n = 1; n <= 9; n++) { if (!used.has(n)) { used.add(n) return n } } return 99 } ranked.push({ rank: takeFree(1), entry: BUILTINS[0] }) if (opts.rdmAudit === true) ranked.push({ rank: takeFree(2), entry: BUILTINS[1] }) if (opts.rdmCockpit === true) ranked.push({ rank: takeFree(3), entry: BUILTINS[2] }) for (const entry of unranked) ranked.push({ rank: takeFree(9), entry }) return ranked .filter(item => item.rank <= 9) .sort((a, b) => a.rank - b.rank) .slice(0, 9) .map((item, i) => ({ ...item.entry, shortcut: `⌘${i + 1}` })) }