Why: auditors need to generate and download a watermarked report from the overview, and that must not appear when the export commands are off or change whether the workspace itself opens. Evidence: SELF npm test, 138 passed. Playwright download case is in e2e/rdm-audit.playwright.ts and runs under scripts/e2e.sh. Impact: the overview shows type, generate, draft confirmation, watermark, sha256, expiry, and download only when the three export commands are enabled. Refs: RDM pull request 301 Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
4.0 KiB
TypeScript
111 lines
4.0 KiB
TypeScript
// 体系工具排位:服务端声明的工作区对所有用户可见,按 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))
|
||
}
|
||
|
||
const BUILTINS: Omit<WorkspaceEntry, 'shortcut'>[] = [
|
||
{ key: 'agents', title: '代理', icon: '🤖' },
|
||
{ key: 'rdm-audit', 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
|
||
}
|
||
|
||
export function buildWorkspaceEntries(server: WorkspaceSource[], opts: BuildWorkspaceOptions = {}): WorkspaceEntry[] {
|
||
const used = new Set<number>()
|
||
const ranked: { rank: number; entry: Omit<WorkspaceEntry, 'shortcut'> }[] = []
|
||
const unranked: Omit<WorkspaceEntry, 'shortcut'>[] = []
|
||
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] })
|
||
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}` }))
|
||
}
|