feat(frontend): 统一 Dify 式插件中心 — RSS 源库+模板收口 [epic #25] - #26
Conversation
…brary + templates [#22 #23] Converges scattered "installable library" UI into one Plugin Hub at /plugins, matching the Dify-style pattern the product is moving toward. - New frontend/app/(app)/plugins/page.tsx: single-page hub with subtype tabs (源库/模板/工具/Agent/触发器/扩展), pill-tab bar visually matching RouteTabs but state-driven off ?type= (RouteTabs itself is strictly usePathname()-driven and doesn't apply to a single-page hub). - 源库: RSS OPML catalog import (previously "导入 RSS 源库" on sources/page.tsx) extracted to components/plugins/rss-catalog-import-dialog.tsx and moved here — it's a catalog/install action, not instance management. sources/page.tsx now only manages already-installed data source instances; the button, dialog, mutation, and RSS_CATALOG_* constants were removed from it. - 模板: studio's template grid + create-from-template flow extracted to components/plugins/template-catalog.tsx and rendered here as the canonical catalog entry. studio/templates/page.tsx is intentionally left untouched (still a standalone entry point reachable from Studio), so the two currently carry duplicated logic by design for this skeleton PR — a follow-up can fold it down to reuse the shared component once the hub is confirmed canonical. - 工具/Agent/触发器/扩展: EmptyState "即将上线" placeholders only, no backend/data wiring yet. - Sidebar: added "插件中心" nav entry (/plugins) under the 构建 group, plus a breadcrumb label in ROUTE_LABELS. tsc --noEmit and eslint both pass with no new errors/warnings.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
✅ Health: 9.7 📋 At a glance 🚨 Change risk: 9.5/10 (high)
📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-07-18 16:24 UTC |
There was a problem hiding this comment.
Code Review
This pull request introduces a new "Plugin Hub" (插件中心) page to centralize the discovery and installation of plugins, including data sources and templates. It refactors the RSS catalog import dialog and template catalog into standalone components under frontend/components/plugins/ and updates the navigation. Feedback focuses on optimizing the Next.js App Router implementation: wrapping the page in <Suspense> to avoid deopting to client-side rendering due to useSearchParams(), and refactoring the tab buttons to use Next.js <Link> components to preserve native browser behaviors and improve SEO.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| import { useState } from 'react' | ||
| import { useRouter, useSearchParams } from 'next/navigation' |
There was a problem hiding this comment.
| function PluginTypeTabs({ active, onSelect }: { active: PluginSubtype; onSelect: (next: PluginSubtype) => void }) { | ||
| return ( | ||
| <nav aria-label="插件类型" className="inline-flex w-fit items-center gap-1 rounded-full bg-muted p-1"> | ||
| {SUBTYPES.map((subtype) => { | ||
| const isActive = subtype.key === active | ||
| return ( | ||
| <button | ||
| key={subtype.key} | ||
| type="button" | ||
| aria-current={isActive ? 'page' : undefined} | ||
| onClick={() => onSelect(subtype.key)} | ||
| className={cn( | ||
| 'relative overflow-hidden rounded-full px-4 py-1.5 text-sm font-medium transition-colors', | ||
| isActive ? 'text-primary-foreground' : 'text-muted-foreground hover:text-foreground', | ||
| )} | ||
| > | ||
| {isActive ? ( | ||
| <motion.span | ||
| layoutId="plugin-hub-tab-pill" | ||
| className="absolute inset-0 rounded-full bg-primary" | ||
| transition={{ type: 'spring', stiffness: 460, damping: 38, mass: 0.6 }} | ||
| /> | ||
| ) : null} | ||
| <span className="relative">{subtype.label}</span> | ||
| <Ripple /> | ||
| </button> | ||
| ) | ||
| })} | ||
| </nav> | ||
| ) | ||
| } |
There was a problem hiding this comment.
当前 PluginTypeTabs 使用了 <button> 并通过 onClick 手动调用 router.push 来切换 URL 参数。这种做法会破坏浏览器的原生链接行为(例如:无法通过鼠标中键在新标签页中打开、无法右键复制链接),同时也对 SEO 爬虫不友好。
建议将 <button> 替换为 Next.js 的 <Link> 组件,并提前计算好每个 Tab 的 href。
function PluginTypeTabs({ active, searchParams }: { active: PluginSubtype; searchParams: ReturnType<typeof useSearchParams> }) {
return (
<nav aria-label="插件类型" className="inline-flex w-fit items-center gap-1 rounded-full bg-muted p-1">
{SUBTYPES.map((subtype) => {
const isActive = subtype.key === active
const params = new URLSearchParams(searchParams.toString())
if (subtype.key === 'datasource') {
params.delete('type')
} else {
params.set('type', subtype.key)
}
const query = params.toString()
const href = query ? `/plugins?${query}` : '/plugins'
return (
<Link
key={subtype.key}
href={href}
scroll={false}
aria-current={isActive ? 'page' : undefined}
className={cn(
'relative overflow-hidden rounded-full px-4 py-1.5 text-sm font-medium transition-colors',
isActive ? 'text-primary-foreground' : 'text-muted-foreground hover:text-foreground',
)}
>
{isActive ? (
<motion.span
layoutId="plugin-hub-tab-pill"
className="absolute inset-0 rounded-full bg-primary"
transition={{ type: 'spring', stiffness: 460, damping: 38, mass: 0.6 }}
/>
) : null}
<span className="relative">{subtype.label}</span>
<Ripple />
</Link>
)
})}
</nav>
)
}
| export default function PluginHubPage() { | ||
| const router = useRouter() | ||
| const searchParams = useSearchParams() | ||
| const rawType = searchParams.get('type') | ||
| const active: PluginSubtype = isPluginSubtype(rawType) ? rawType : 'datasource' | ||
|
|
||
| function selectSubtype(next: PluginSubtype) { | ||
| const params = new URLSearchParams(searchParams.toString()) | ||
| if (next === 'datasource') params.delete('type') | ||
| else params.set('type', next) | ||
| const query = params.toString() | ||
| router.push(query ? `/plugins?${query}` : '/plugins', { scroll: false }) | ||
| } | ||
|
|
||
| return ( | ||
| <PageContainer | ||
| eyebrow="Plugin Hub" | ||
| title="插件中心" | ||
| description="像应用市场一样浏览、安装和管理可插拔能力——源库、模板、工具、Agent、触发器与扩展统一入口。" | ||
| tabs={<PluginTypeTabs active={active} onSelect={selectSubtype} />} | ||
| > | ||
| {active === 'datasource' ? ( | ||
| <DatasourceLibraryTab /> | ||
| ) : active === 'template' ? ( | ||
| <TemplateCatalog /> | ||
| ) : ( | ||
| <EmptyState title="即将上线" description={PLACEHOLDER_DESCRIPTION[active]} /> | ||
| )} | ||
| </PageContainer> | ||
| ) | ||
| } |
There was a problem hiding this comment.
在 Next.js App Router 中,直接在页面组件(Page Component)中使用 useSearchParams() 会导致整页在构建时退化为完全客户端渲染(deopt to client-side rendering),从而失去静态生成(SSG)的优势。
建议将依赖 useSearchParams 的逻辑抽离到子组件中,并在页面组件中用 <Suspense> 进行包裹。同时,由于我们将 PluginTypeTabs 改为了声明式的 <Link>,这里也可以移除手动的 router.push 逻辑,使代码更加简洁。
function PluginHubContent() {
const searchParams = useSearchParams()
const rawType = searchParams.get('type')
const active: PluginSubtype = isPluginSubtype(rawType) ? rawType : 'datasource'
return (
<PageContainer
eyebrow="Plugin Hub"
title="插件中心"
description="像应用市场一样浏览、安装和管理可插拔能力——源库、模板、工具、Agent、触发器与扩展统一入口。"
tabs={<PluginTypeTabs active={active} searchParams={searchParams} />}
>
{active === 'datasource' ? (
<DatasourceLibraryTab />
) : active === 'template' ? (
<TemplateCatalog />
) : (
<EmptyState title="即将上线" description={PLACEHOLDER_DESCRIPTION[active]} />
)}
</PageContainer>
)
}
export default function PluginHubPage() {
return (
<Suspense fallback={<div className="p-6 text-center text-sm text-muted-foreground">加载中...</div>}>
<PluginHubContent />
</Suspense>
)
}
|
Coordination review (2026-07-19): converted this PR to Draft while the frontend baseline is being converged. Verified state:
Convergence path:
Do not merge this stacked PR as-is. |
Epic #25 落地骨架。方向纠偏后 (RSS 源库/模板不各拆, 统一收进 Dify 式 Plugin 中心) 的第一步。基于 codex/rss-provider-ecosystem 分支。
What
/plugins统一插件中心, 子类型 tab: 源库 / 模板 / 工具 / Agent / 触发器 / 扩展 (参照 Dify Marketplace 类型划分)components/plugins/rss-catalog-import-dialog.tsxcomponents/plugins/template-catalog.tsx作为中心内规范入口/plugins, Blocks 图标)设计决策 (Fable 审计已核)
usePathname()驱动, 不适用单页多子类型 → 手搓 pill-tab 视觉对齐 RouteTabs (同类名/同 layoutId 滑块动画/Ripple), 状态走?type=参数 (可分享/可链接)。未动 route-tabs.tsxTest
tsc --noEmitexit 0 零错误;lintexit 0 零新警告 (9 个存量警告在未触碰文件)骨架边界 (后续跟进)
studio/templates/page.tsx原样保留 (out of scope), 与 TemplateCatalog 暂重复; 后续可折叠为共享组件