☰
SkillHub 技能详情与审核页文件浏览侧边栏:从平铺列表到目录树 + 弹窗预览的完整实现解析
2026/10/14 15:41:32 网站建设 项目流程
  • 人工智能
  • 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.

项目地址:https://gitcode.com/gh_mirrors/sk/skillhub
点击查看免费下载

本文基于 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 界定的本期实现范围包括:

  1. 在技能详情页与审核详情页中新增常驻文件浏览侧边栏;
  2. 将现有平铺files列表重构为目录树,支持文件夹展开 / 合并;
  3. 点击文件节点后,通过弹窗预览文件内容;
  4. 支持 Markdown 文档渲染、常见文本文件源码预览,以及大文件 / 不支持类型的不可预览提示;
  5. 对不可预览文件提供下载入口;
  6. 桌面端使用右侧侧边栏布局,移动端将文件浏览区域下沉到主内容区域下方。

同时 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实现展示了第二道防线:

  1. loadAuthorizedContext(reviewId, userId, userNsRoles)加载审核任务与命名空间,调用reviewService.canViewReview(...)校验当前用户是否具备审核查看权限,不满足则抛出DomainForbiddenException("review.no_permission");
  2. 鉴权通过后,用审核任务绑定的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 列出三类技术风险,均可在仓库中找到对应缓解实现:

  1. 审核页缺少任意文件正文读取能力可能导致联调返工→ 通过独立的GET /api/v1/reviews/{id}/file接口 + 前端useReviewFilehook 补齐,契约清晰;
  2. 第三方文件树组件可能与 Tailwind 风格不匹配→ 最终采用自研轻量树(file-tree-builder.ts+file-tree-node.tsx),与现有 UI 体系零冲突;
  3. 类型判断 / 大文件判定不统一→ 统一收敛到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.

项目地址:https://gitcode.com/gh_mirrors/sk/skillhub
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询