- 人工智能
- AI Agent
- AI 技能
- 后端
- 前端
【免费下载链接】skillhub
Self-hosted, open-source agent skill registry for enterprises. Publish & version skill packages, govern with RBAC and audit logs, deploy on-premise with Docker or Kubernetes.
本文基于 SkillHub 仓库中的 PRD 文档 skill-file-browser-sidebar-v1.0-prd.md,系统讲解“技能详情页 / 审核详情页文件浏览侧边栏”这一功能的产品设计、数据模型、预览判定规则与前后端实现。读完本文,你将掌握:如何把后端返回的平铺SkillFile[]列表在纯前端构建为可展开的目录树;如何统一“可预览 / 不可预览 / 可下载”的判定规则;以及如何通过一条共享的文件读取链路,让技能详情页与审核详情页具备一致的文件预览能力,且不扩大任何权限边界。
1. 背景与要解决的业务问题
SkillHub 是一个自托管、面向企业的 Agent 技能(skill)注册中心,用户可以发布并版本化 skill 包。在 PRD 落地之前,技能详情页与审核详情区块都提供“概览 / 文件 / 版本”三个 Tab,但“文件”Tab 仅展示一份平铺文件列表,存在三个明确的问题(见 PRD 的“需求说明”一节):
- 平铺列表无法反映目录层级:用户难以理解 skill 包内部的结构;
- 审核人员无法点开任意文件:审核页此前仅返回
files、documentationPath、documentationContent三个字段,审核人员只能依赖 README 或下载整个 zip 包离线查看; - 跨 Tab 查看成本高:如果继续把文件浏览塞进“文件”Tab,用户在“概览”或“版本”上下文中想看某个文件就需要来回切换。
因此 PRD 给出的目标是:用统一的“常驻侧边文件浏览 + 弹窗预览”体验取代平铺文件列表,让浏览者、作者/命名空间成员、审核人员与管理员在不下载 zip 包的前提下,快速查看 skill 的目录结构与任意文件内容。
1.1 目标用户与价值主张
| 用户角色 | 使用场景 |
|---|---|
| 浏览技能详情的普通用户 | 快速了解 skill 包结构,按需查看具体文件 |
| 作者 / 命名空间成员 | 发布后自查包内容 |
| 审核人员 / 管理员 | 在审核页点开任意文件验证实现内容,而非仅看 README |
价值主张有三点:降低理解 skill 包结构的成本;在详情页与审核页提供一致的文件预览能力,减少下载与上下文切换;保持现有三个 Tab 的主语义不变,同时让用户在任意 Tab 下都能快速查看文件。
2. 功能范围与设计决策
2.1 本期范围
PRD 界定的本期实现范围包括:
- 在技能详情页与审核详情页中新增常驻文件浏览侧边栏;
- 将现有平铺
files列表重构为目录树,支持文件夹展开 / 合并; - 点击文件节点后,通过弹窗预览文件内容;
- 支持 Markdown 文档渲染、常见文本文件源码预览,以及大文件 / 不支持类型的不可预览提示;
- 对不可预览文件提供下载入口;
- 桌面端使用右侧侧边栏布局,移动端将文件浏览区域下沉到主内容区域下方。
同时 PRD 明确了不包含的内容:侧边栏内置文件名搜索 / 路径过滤;详情页中独立切换文件浏览版本;审核页版本维度切换浏览;图片、音视频、富二进制文件的内联预览;超大文件截断预览。
2.2 交互决策:常驻导航 + 弹窗预览
PRD 的“设计决策”部分解释了为什么采用“常驻文件导航 + 弹窗预览”模式,而不是切换主内容 Tab 或在侧边栏内直接阅读正文:
- 保持现有三个 Tab 的语义稳定(概览:README/文档主体;文件:树本身可作为补充说明区域;版本:版本列表与生命周期信息);
- 允许用户在“概览”或“版本”上下文中快速查看文件;
- 更适合移动端,把阅读行为独立到弹窗层处理。
响应式决策要求桌面端为右侧常驻侧边栏、移动端为主内容下方文件浏览区 + 接近全屏的预览弹层,并保证 320 / 375 / 414 / 768 / 1024 / 1440 等常见宽度下无异常横向滚动。样式上延续数据密集型管理界面风格,交互动效控制在 150–300ms 并尊重prefers-reduced-motion。
3. 数据模型与文件树构建
3.1 输入数据:平铺 SkillFile[] 由前端建树
PRD 的前端约束明确:文件树由前端从平铺文件列表构建,不要求后端返回嵌套目录结构。后端的文件列表接口(SkillController.java 中GET /api/v1/skills/{namespace}/{slug}/versions/{version}/files)返回的正是平铺结构,每个条目包含id、filePath、fileSize、contentType、sha256字段。
树构建逻辑位于 file-tree-builder.ts。核心节点模型为:
export interface FileTreeNode { id: string // 以完整路径作为唯一标识 name: string // 显示名(文件名或目录名) path: string // 从根开始的完整路径 type: 'file' | 'directory' file?: SkillFile // 仅文件节点,保留原始元数据 children?: FileTreeNode[] // 仅目录节点 depth: number // 嵌套深度,根级为 0 }buildFileTree(files)的算法要点(对应 file-tree-builder.ts):
- 先按
filePath对输入做 locale 排序,保证输出顺序稳定; - 逐段拆分路径,用
Map<string, FileTreeNode>(directoryMap)记录已创建的目录节点,遇到重复路径时复用已有目录节点,避免同一目录被重复创建; - 每段路径决定是目录还是文件:最后一层生成
type: 'file'节点并挂载原始SkillFile,其余层生成 / 复用目录节点并逐层深入; - 最后对每一层递归排序:目录在前、文件在后,同级内按名称字母序(
sortTreeNodes实现)。
3.2 树渲染:递归节点组件与性能控制
树组件 file-tree.tsx 对外暴露files、onFileClick、bare三个 props:bare模式用于嵌入 Card(技能详情页侧边栏即采用此模式),标准模式则自带带文件计数的标题栏。两个性能细节值得注意:
useMemo(() => buildFileTree(files), [files])缓存树结构,避免每次渲染重建;- 文件节点组件 file-tree-node.tsx 用
memo包裹,防止父级状态更新引发整树重渲染——这正是 PRD 性能约束中“目录树展开 / 收起应保持即时响应”的落地手段。
节点渲染规则:文件节点显示类型图标(由getFileIcon按扩展名映射到 Lucide 图标)+ 等宽字体文件名 + 人类可读的文件大小(formatFileSize处理 B/KB/MB 三档);目录节点显示展开/收起箭头与 Folder/FolderOpen 图标,点击切换isExpanded状态,缩进由depth * 16 + 12像素内边距控制。
一个值得对照的细节:PRD 要求“第一层目录默认展开”,而组件通过defaultExpandedprop 提供了该能力(file-tree-node.tsx 中useState(defaultExpanded))。从当前file-tree.tsx的调用看,根级节点传入的是defaultExpanded={false},即默认收起——这是需求与实现之间的一处差异点,可以作为后续体验完善的候选项。
4. 可预览性判定:统一规则避免详情页与审核页行为漂移
PRD 的技术风险中明确提到“文件类型判断与大文件判定策略若不统一,可能导致详情页与审核页行为不一致”,缓解措施是“先定义统一的可预览 / 不可预览 / 可下载判定规则,再进入实现”。该规则集中在 file-type-utils.ts 中:
4.1 判定常量与规则
| 规则 | 取值 | 说明 |
|---|---|---|
| 预览大小上限 | MAX_PREVIEW_SIZE = 1MB(1024 × 1024 字节) | 超过即不可预览 |
| 可预览扩展名 | md/mdx/markdown、ts/tsx/js/jsx/json/yaml/yml、py/java/go/rs/c/cpp/h/hpp、sh/bash/zsh/fish、txt/xml/xsd/xsl/dtd/toml/ini/env、html/css/scss/sass/less/vue/svelte | 见 file-type-utils.ts |
| 二进制扩展名 | 图片(png/jpg/gif/bmp/ico/svg)、视频、音频、压缩包(zip/tar/gz/rar/7z)、Office 文档、可执行文件 | 直接判定不可预览 |
| 无扩展名文件 | 视为可预览 | ext === ''时放行 |
canPreviewFile(fileName, fileSize)返回结构化结果,把 PRD 要求的“便于前端区分不可预览原因”落实为三种 reason:
{ canPreview: boolean; reason?: 'too-large' | 'binary' | 'unsupported' }判定顺序:先查大小(too-large)→ 再查二进制扩展名(binary)→ 最后查可预览扩展名白名单(unsupported)。此外还提供getFileTypeLabel(侧边栏/弹窗中的类型徽标文案)与getLanguageForHighlight(把扩展名映射为 highlight.js 语言标识,如py → python、yml → yaml、md → markdown、txt → plaintext,未知类型返回null)。
对应的单元测试见 file-type-utils.test.ts,树构建逻辑见 file-tree-builder.test.ts。
4.2 语法高亮的第二道阈值
预览弹窗 file-preview-dialog.tsx 中还有一层 PRD 未直接规定但合理的性能控制:语法高亮阈值 500KB——即使文件通过 1MB 的可预览判定,只有当文件 ≤ 500KB 且能解析出高亮语言时才走CodeRenderer高亮渲染,否则回退为pre + code纯文本(whitespace-pre-wrap break-words),避免对接近 1MB 的大文本做昂贵的逐 token 着色。
5. 技能详情页集成:共享读取链路与侧边栏布局
5.1 从“README 专用读取”抽象为“任意文件读取”
PRD 要求把 README 专用的单文件读取能力抽象成任意文件读取查询。在 use-skill-queries.ts 中可以看到这条链路:
useSkillFiles(namespace, slug, version):拉取当前版本的平铺文件列表,queryKey 为['skills', ns, slug, 'versions', version, 'files'],是侧边栏的数据源;useSkillReadme(...):README 场景,按解析出的文档路径读取;useSkillFile(namespace, slug, version, filePath, enabled):抽象后的任意文件读取查询,queryKey 包含filePath,staleTime为 5 分钟,实现“文件预览按需加载,不一次性拉取所有正文”的性能约束。
三者共用同一个底层函数getSkillDocumentation,最终请求:
GET /api/v1/skills/{namespace}/{slug}/versions/{version}/file?path={encodeURIComponent(path)}后端实现见 SkillController.java:getFileContent从对象存储流式返回文件,Content-Type为application/octet-stream,并经过与文件列表一致的可见性校验(userId+userNsRoles参数),不新增数据库 schema 变更。
5.2 侧边栏布局:桌面双栏、移动端下沉
在 skill-detail.tsx 中,文件浏览侧边栏以<aside className="w-full lg:w-80 flex-shrink-0 space-y-5">呈现:
- 桌面端(
lg:断点):主内容区与侧边栏双栏布局,aside固定宽度 20rem; - 移动端:
w-full使侧边栏排到主内容下方,形成单列流式布局,无强制双栏; - 侧边栏是一张可折叠的 Card:头部显示文件夹图标、
fileTree.title文案、文件总数与展开箭头,展开后在max-h-[400px] overflow-y-auto容器内渲染<FileTree files={files} onFileClick={handleFileClick} bare />; - 点击文件节点设置
previewNode并打开FilePreviewDialog,由useSkillFile按需拉取正文。
“文件”Tab 内仍保留<FileTree files={files} onFileClick={handleFileClick} />(skill-detail.tsx),符合 PRD“文件树本身可作为主内容补充或说明区域,但不再是唯一的文件入口”的定位。
6. 审核页能力补齐:新接口 + 权限边界
这是 PRD 中工作量最大也最关键的一环:审核详情此前只有files、documentationPath、documentationContent,需要补齐“按路径读取任意文件正文”的能力。PRD 给出两条可接受路径(新增审核专用文件读取接口,或扩展 review detail 数据链路),仓库采用的是新增审核场景专用接口:
GET /api/v1/reviews/{id}/file?path={encodeURIComponent(path)}6.1 控制器层:路径穿越防护
ReviewController.java 的getReviewFile在进入业务逻辑前先做路径合法性校验:path为空白、包含..、或以/开头的请求一律返回 400。这正是 PRD 安全约束“不可通过构造任意路径越权读取 skill 包之外的内容”的第一道防线。
6.2 服务层:复用审核鉴权上下文
ReviewSkillDetailAppService.java 的getReviewFileContent实现展示了第二道防线:
loadAuthorizedContext(reviewId, userId, userNsRoles)加载审核任务与命名空间,调用reviewService.canViewReview(...)校验当前用户是否具备审核查看权限,不满足则抛出DomainForbiddenException("review.no_permission");- 鉴权通过后,用审核任务绑定的
skillVersionId调用skillQueryService.getFileContentByVersionId(versionId, filePath)读取文件内容。
也就是说,文件读取被限定在当前审核上下文绑定的 skill 版本上,与 PRD 后端约束完全一致。前端对应 use-review-file.ts 中的useReviewFile(reviewId, filePath, enabled)查询(同样 5 分钟缓存、弹窗打开时才enabled),以及 review-skill-detail-section.tsx 中:
- 三个 Tab(概览 / 文件 / 版本)之外常驻一个
FilePreviewDialog,点击任意文件即弹窗预览,与详情页交互对齐; - 单文件下载通过构造
GET /api/v1/reviews/{id}/file?path=...链接并触发<a download>完成(review-skill-detail-section.tsx),同一个接口同时承担了“文本预览”与“单文件下载”两种用途——这在控制器 Javadoc 中亦有明确说明。
7. 文件预览弹窗:渲染策略与状态处理
FilePreviewDialog 是详情页与审核页共用的预览层,其渲染分支完整覆盖了 PRD 的“文件预览行为”要求:
| 状态 | 呈现 |
|---|---|
isLoading | 居中 spinner,对应“按需加载”语义 |
error | 错误提示 +error.message |
不可预览(too-large/binary/unsupported) | 分别显示“文件过大,暂不支持预览”“当前文件类型暂不支持预览”等文案(i18n key:filePreview.tooLarge/filePreview.binaryFile/filePreview.unsupported),并提供下载按钮 |
| Markdown 文件 | 复用 MarkdownRenderer 按文档样式渲染 |
| 可高亮文本(≤500KB 且有语言映射) | 走 CodeRenderer 语法高亮 |
| 其他文本 | <pre><code>纯文本,保留可滚动阅读体验 |
其他细节与 PRD 的对应关系:
- 路径显示:弹窗 footer 以等宽字体显示
node.path,满足“预览弹窗需要显示当前文件路径,便于用户确认查看对象”; - 头部信息:文件名 + 类型徽标(
getFileTypeLabel)+ 复制 / 下载 / 关闭三个操作,复制按钮带 idle → spinning → done 的动画反馈; - 弹窗尺寸:
w-[min(calc(100vw-2rem),72rem)] max-h-[90vh],在移动端接近全屏,桌面端限宽,内容区overflow-auto独立滚动——对应 PRD“长文本滚动区域难用”的交互风险缓解; - 开源组件策略:PRD 要求优先复用现有
react-markdown、rehype-highlight与 Dialog 基础能力、避免引入风格冲突的重型组件。从实现看该决策被遵守:弹窗基于 dialog.tsx 自建,Markdown 与代码渲染全部复用既有组件,没有引入新的文件树/代码查看第三方库。
对应的组件级测试见 file-preview-dialog.test.ts 与 file-tree.test.ts。
8. 约束、风险与验收
8.1 PRD 中的关键约束回顾
- 前端:兼容现有详情页/审核页结构,复用 Dialog、MarkdownRenderer、i18n、TanStack Query 模式;不因新组件导致样式体系漂移;
- 后端:允许补充新接口,但避免数据库 schema 变化;文件读取限定在审核上下文可访问的版本上;
- 性能:打开文件树不阻塞首屏渲染;文件预览按需加载;展开/收起即时响应(
useMemo建树 +memo节点已落实); - 国际化:文件浏览、预览、不可预览、大文件提示、下载操作等文案均需中英文覆盖。
8.2 风险评估与缓解(PRD 原文的落地情况)
PRD 列出三类技术风险,均可在仓库中找到对应缓解实现:
- 审核页缺少任意文件正文读取能力可能导致联调返工→ 通过独立的
GET /api/v1/reviews/{id}/file接口 + 前端useReviewFilehook 补齐,契约清晰; - 第三方文件树组件可能与 Tailwind 风格不匹配→ 最终采用自研轻量树(
file-tree-builder.ts+file-tree-node.tsx),与现有 UI 体系零冲突; - 类型判断 / 大文件判定不统一→ 统一收敛到
file-type-utils.ts,详情页与审核页共用同一套canPreviewFile规则。
8.3 验收标准(摘要)
PRD 定义的功能验收要点:详情页与审核页三个 Tab 下均可见侧边栏;平铺列表正确转换为目录树且支持展开/收起;点击文件打开预览弹窗;Markdown 正确渲染;文本文件以源码/纯文本展示;二进制或超大文件显示不可预览提示并提供下载;审核页可预览任意文件而非仅 README。质量验收则要求桌面右侧栏 / 移动端下沉布局、常见断点无横向滚动、不影响既有 Tab 内容、完成中英文 i18n。
9. 小结与后续扩展方向
该功能以一份共享前端模型(FileTreeNode+buildFileTree+canPreviewFile)、两个按需读取查询(useSkillFile/useReviewFile)和一条弹窗组件(FilePreviewDialog),在不改动数据库、不扩大权限边界的前提下,把详情页与审核页的文件浏览体验对齐。核心源码与测试索引:
- 树构建:file-tree-builder.ts、file-tree.tsx、file-tree-node.tsx
- 预览判定:file-type-utils.ts、file-preview-dialog.tsx
- 前端集成:skill-detail.tsx、review-skill-detail-section.tsx、use-skill-queries.ts、use-review-file.ts
- 后端接口:SkillController.java、ReviewController.java、ReviewSkillDetailAppService.java
PRD 同时记录了后续可扩展方向,可作为二期输入:按版本浏览文件树并结合权限控制设计访问策略;侧边栏搜索过滤、最近打开文件、选中文件高亮;对 JSON / YAML / TS / JS 提供更完整的语法高亮;以及基于服务端结构化元数据的超大文本文件受控截断预览。这些方向均建立在本文所述的共享模型之上,扩展成本可控。
- 人工智能
- AI Agent
- AI 技能
- 后端
- 前端
【免费下载链接】skillhub
Self-hosted, open-source agent skill registry for enterprises. Publish & version skill packages, govern with RBAC and audit logs, deploy on-premise with Docker or Kubernetes.
相关推荐
SkillHub 前端安全审核信息展示:从 PRD 到审核页、技能详情页的完整实现
SkillHub 前端安全审核信息展示:从 PRD 到审核页、技能详情页的完整实现 本篇围绕 SkillHub 的 PRD《前端安全审核信息展示》( secur
人工智能AI AgentAI 技能后端前端零基础上手Dear ImGui文件浏览器:从目录树到文件列表的完整指南
零基础上手Dear ImGui文件浏览器:从目录树到文件列表的完整指南 你还在为C++项目手动编写文件浏览界面?还在纠结跨平台GUI控件的兼容性问题?本文将带你
UI组件前端桌面应用图形学RSUITE Sidenav 与 Modal 组合实战:在弹窗内构建文件浏览器式侧边栏布局
RSUITE Sidenav 与 Modal 组合实战:在弹窗内构建文件浏览器式侧边栏布局 Sidenav 是 rsuite 对页面侧边栏 Nav 的封装,通常
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考