Skip to content

feat(frontend): 统一 Dify 式插件中心 — RSS 源库+模板收口 [epic #25] - #26

Merged
2233admin merged 1 commit into
codex/rss-provider-ecosystemfrom
feat/frontend-plugin-hub
Jul 23, 2026
Merged

feat(frontend): 统一 Dify 式插件中心 — RSS 源库+模板收口 [epic #25]#26
2233admin merged 1 commit into
codex/rss-provider-ecosystemfrom
feat/frontend-plugin-hub

Conversation

@2233admin

Copy link
Copy Markdown
Owner

Epic #25 落地骨架。方向纠偏后 (RSS 源库/模板不各拆, 统一收进 Dify 式 Plugin 中心) 的第一步。基于 codex/rss-provider-ecosystem 分支。

What

  • 新建 /plugins 统一插件中心, 子类型 tab: 源库 / 模板 / 工具 / Agent / 触发器 / 扩展 (参照 Dify Marketplace 类型划分)
  • 源库 tab: RSS 导入能力从 sources 页迁入, 抽成 components/plugins/rss-catalog-import-dialog.tsx
  • 模板 tab: 抽 components/plugins/template-catalog.tsx 作为中心内规范入口
  • 其余四类占位 EmptyState「即将上线」
  • sources 页移除「导入 RSS 源库」按钮 — 源库是目录动作归中心, sources 页只管数据源实例 (表格/分页逻辑原样保留, RSS 残留清零)
  • 侧栏加「插件中心」入口 (/plugins, Blocks 图标)

设计决策 (Fable 审计已核)

  • RouteTabs 是 usePathname() 驱动, 不适用单页多子类型 → 手搓 pill-tab 视觉对齐 RouteTabs (同类名/同 layoutId 滑块动画/Ripple), 状态走 ?type= 参数 (可分享/可链接)。未动 route-tabs.tsx
  • ADR 0014/0015 的 "plugin" 指后端能力 manifest (权限/健康探针), 与本前端内容目录 (源库/模板) 是不同概念, 无冲突; 命名 reviewer 后续可消歧

Test

  • tsc --noEmit exit 0 零错误; lint exit 0 零新警告 (9 个存量警告在未触碰文件)
  • Fable 抽查: 数据源表格 9 处关键引用完整、RSS 残留 0、侧栏入口就位、PageContainer 支持 tabs prop

骨架边界 (后续跟进)

  • 工具/Agent/触发器/扩展 tab 仅占位, 未接数据
  • studio/templates/page.tsx 原样保留 (out of scope), 与 TemplateCatalog 暂重复; 后续可折叠为共享组件
  • 可发现性缺口: 旧 RSS 导入成功后会把 sources 页筛到 disabled 让用户看到新源; 迁入中心后该页内状态断了。导入后仅 toast 计数, 无直达新建 (disabled) 源的路径。建议 follow-up: toast action 链到 /sources

…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.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 53513e0c-1fe0-4547-86d5-2658c4381a2a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@repowise-bot

repowise-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

✅ Health: 9.7

📋 At a glance
4 new findings introduced.

🚨 Change risk: 9.5/10 (high)
This change's risk is driven by:

  • more lines added than baseline
  • more scattered than baseline

📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-07-18 16:24 UTC
Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +3 to +4
import { useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

为了支持 <Suspense><Link> 组件,我们需要更新 React 和 Next.js 的导入。

Suggested change
import { useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState, Suspense } from 'react'
import { useSearchParams } from 'next/navigation'
import Link from 'next/link'

Comment on lines +45 to +75
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>
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

当前 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>
  )
}

Comment on lines +111 to +141
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>
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在 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>
  )
}

@2233admin
2233admin marked this pull request as draft July 18, 2026 21:43

Copy link
Copy Markdown
Owner Author

Coordination review (2026-07-19): converted this PR to Draft while the frontend baseline is being converged.

Verified state:

  • this PR contains one focused frontend commit and changes 5 files;
  • its base is codex/rss-provider-ecosystem@a3657cb, which has no PR and is not in main;
  • compared with current main@36b9b7e, the head is 2 commits ahead / 20 commits behind and the ancestry brings roughly 149 changed files into view;
  • CI is green for the current stacked base, but that does not prove safe integration into the intended 5080 frontend baseline;
  • the existing review still has two valid integration items: wrap the useSearchParams() boundary in Suspense, and use link semantics for shareable tab navigation.

Convergence path:

  1. publish or identify the exact 5080 frontend baseline ref;
  2. create an integration branch from that ref;
  3. cherry-pick only b629ba1 (the plugin-hub commit), rather than merging the unpublished ecosystem base wholesale;
  4. resolve the two Next.js review items and the post-import discoverability gap;
  5. run frontend lint/typecheck/build plus browser smoke coverage, then retarget or replace this PR.

Do not merge this stacked PR as-is.

@2233admin
2233admin merged commit b629ba1 into codex/rss-provider-ecosystem Jul 23, 2026
5 checks passed
@2233admin
2233admin deleted the feat/frontend-plugin-hub branch July 24, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant