Skip to content

feat: 新增CMS资源自定义管理功能 & 重构主题引入多套现代化主题#465

Merged
consistent-k merged 1 commit into
mainfrom
dev
Apr 18, 2026
Merged

feat: 新增CMS资源自定义管理功能 & 重构主题引入多套现代化主题#465
consistent-k merged 1 commit into
mainfrom
dev

Conversation

@consistent-k
Copy link
Copy Markdown
Owner

@consistent-k consistent-k commented Apr 18, 2026

Summary by CodeRabbit

发布说明

  • 新功能

    • 新增CMS管理界面,支持自定义添加、编辑和删除视频源
    • 推出三款新主题:Vercel、Airbnb、Claude 风格,一键切换
    • 重设设置页面为多卡片布局,优化用户体验
  • 样式优化

    • 升级全局字体为 Geist,优化排版渲染
    • 刷新设计色彩体系和视觉令牌
    • 改进视频列表为响应式网格布局
    • 优化各组件样式和间距
  • Bug 修复

    • 修复中间件响应处理逻辑

@github-actions
Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

概述

本PR增强了VodHub的CMS管理能力,引入了自定义CMS源支持。核心变更包括:将前端存储迁移至Zustand框架、重构后端代理路由为基于操作的分发、全面升级设计系统(字体、颜色、阴影令牌)、新增CMS管理UI组件,以及更新文档以反映新的开发指南。

变更详情

队列/文件 变更摘要
文档更新
CLAUDE.md, README.md
扩展开发指南,文档化新的CMS管理端点、代理配置、CMS路由约定;更新功能特性描述,突出内置CMS支持和主题切换。
后端中间件与路由
apps/backend/src/middleware/jsonReturn.ts, apps/backend/src/routes/proxy.ts
jsonReturn中间件添加提前返回逻辑;proxy路由重构为基于操作的分发器,替换通用代理实现为CMS处理程序路由(home、homeVod、detail、play、search、category)。
前端存储迁移至Zustand
apps/frontend/src/lib/store/useSettingStore.ts, apps/frontend/src/lib/store/useCmsStore.ts, apps/frontend/src/lib/store/useThemeStore.ts, apps/frontend/src/lib/store/useVodSitesStore.ts
将store2替换为Zustand创建持久化存储;新增useCmsStore管理自定义CMS的CRUD操作和本地存储;更新getVodTypes签名移除force参数,集成CMS列表到站点选项;更改默认主题从midnight到vercel。
CMS管理组件与初始化
apps/frontend/src/components/cms-management/index.tsx, apps/frontend/src/components/InitProvider.tsx
新增CMS管理UI组件,支持表格展示、add/edit/delete/view模态框;getVodTypes调用简化为无参数形式。
前端组件逻辑调整
apps/frontend/src/components/SiteHeader.tsx, apps/frontend/src/components/ThemeProvider.tsx, apps/frontend/src/components/ui/ThemeSelector.tsx
SiteHeader直接读取分离的设置字段,更新调用显式指定vod_hub_api/site_name/current_site;ThemeProvider locale导入路径更新;ThemeSelector移除外层布局包装和静态标题。
VodList网格布局重构
apps/frontend/src/components/video/VodList/index.tsx, apps/frontend/src/components/video/VodList/index.module.scss
VodList从平面容器重构为Ant Design Row/Col响应式网格;SCSS类从BEM嵌套改为扁平化显式类名,选择器更新为全类名引用。
组件样式微调
apps/frontend/src/components/BasicLayout.module.scss, apps/frontend/src/components/video/VodPalyer/index.module.scss, apps/frontend/src/components/video/VodSearch/index.tsx, apps/frontend/src/components/video/VodTypes/index.module.scss
字体族变更为Geist/Inter;页脚字号和边框更新;阴影、边框和焦点颜色令牌替换;搜索覆盖层背景为rgba半透明。
页面级样式与布局
apps/frontend/src/pages/home/index.module.scss, apps/frontend/src/pages/home/index.tsx, apps/frontend/src/pages/detail/index.module.scss, apps/frontend/src/pages/category/index.module.scss, apps/frontend/src/pages/setting/index.module.scss, apps/frontend/src/pages/setting/index.tsx
全局设计令牌应用(box-shadow替代border/alpha背景、border-radius调整);首页标题排版和图标颜色更新;设置页面从分步UI改为多卡片布局,新增CMS管理卡片;表单、输入框、按钮的全局覆盖样式。
全局设计系统
apps/frontend/src/globals.scss, apps/frontend/src/lib/themes/index.ts
替换Google字体为Vercel CDN的Geist字体;重新设计色板(从深色系改为浅色系),引入workflow相关颜色和新的阴影/边框令牌系统;主题从midnight/aurora/cyber改为vercel/airbnb/claude;字体平滑和连字符启用。
前端服务与类型
apps/frontend/src/services/vodhub/index.ts, packages/shared/src/types/custom-cms.ts, packages/shared/package.json
vodhub服务新增CMS感知路由逻辑,匹配custom_前缀站点时通过代理端点转发请求;新增CustomCms、CreateCustomCmsInput、UpdateCustomCmsInput类型定义;packages导出新增./types/custom-cms子路径。

序列图

sequenceDiagram
    participant User as 用户
    participant CMSMgmt as CMS管理<br/>组件
    participant CmsStore as useCmsStore<br/>Zustand存储
    participant VodService as vodhub<br/>服务
    participant Backend as 后端代理<br/>路由
    participant CmsHandler as CMS处理程序<br/>(home/detail等)

    User->>CMSMgmt: 打开CMS管理UI
    CMSMgmt->>CmsStore: fetchCmsList()
    CmsStore-->>CMSMgmt: 返回cmsList

    User->>CMSMgmt: 添加新CMS源
    CMSMgmt->>CmsStore: addCms(name, url, desc)
    CmsStore->>CmsStore: 生成UUID,添加时间戳<br/>持久化至localStorage
    CmsStore-->>CMSMgmt: 返回创建的CMS

    User->>CMSMgmt: 选择CMS站点
    CMSMgmt->>CmsStore: 更新setting中的site_name<br/>为custom_${id}

    User->>VodService: homeApi(current_site)
    VodService->>CmsStore: getCmsUrl(custom_${id})
    CmsStore-->>VodService: 返回CMS URL
    VodService->>Backend: /api/vodhub/proxy<br/>headers: {x-proxy-target, x-proxy-action}
    Backend->>CmsHandler: 路由至对应操作处理程序
    CmsHandler-->>Backend: 返回内容
    Backend-->>VodService: JSON响应
    VodService-->>User: 渲染视频列表
Loading

预计审查工作量

🎯 4 (复杂) | ⏱️ ~60 分钟

相关PR

建议标签

frontend, backend, styles

兔子的诗

🐰 跳呀跳呀过代理,
CMS列表闪闪闪,
Zustand存储永不丢,
新色系焕然一新,
VodHub梦想化真实!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR标题明确指出了两个核心变更:新增CMS资源自定义管理功能和重构主题系统。通过检查raw_summary可验证,PR确实包含了新增CMS管理UI、CMS存储、自定义CMS类型定义,以及完整的主题系统重构(从midnight/aurora/cyber替换为vercel/airbnb/claude,更新全局样式和字体等)。标题简洁清晰,准确总结了PR的主要变更。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/frontend/src/pages/setting/index.module.scss (1)

33-128: ⚠️ Potential issue | 🟡 Minor

Stylelint:字体族引号 + 未知伪类

静态分析给出的错误都是真实问题,合并前需处理:

  • font-family: 'Geist', 'Inter', sans-serif;(第 33、63、77、97、126 行)违反 font-family-name-quotes,这类单词形字体名不应带引号。
  • :global(...)(第 61、71、72、90、91、95、103、114、125 行)被判为未知伪类。这通常是因为仓库的 stylelint 没有配 postcss-scss / CSS Modules 语法,可以在 .stylelintrc 里把 :global 加到 selector-pseudo-class-no-unknownignorePseudoClasses;或者同上条建议,把这些规则收敛到局部作用域后再看 lint 情况。
♻️ 字体引号的修复示例
-        font-family: 'Geist', 'Inter', sans-serif;
+        font-family: Geist, Inter, sans-serif;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend/src/pages/setting/index.module.scss` around lines 33 - 128, The
stylesheet uses quoted generic font-family names and unrecognized :global
pseudo-class usages causing Stylelint errors; remove quotes from font-family
entries (update occurrences in the rules containing "font-family: 'Geist',
'Inter', sans-serif;" — e.g., inside the top-level block, .setting-form-content,
:global(.ant-form-item-label) label, :global(.ant-btn), and :global(.ant-card))
and either add ":global" to the selector-pseudo-class-no-unknown
ignorePseudoClasses in your .stylelintrc (so rules like :global(.ant-input) are
accepted) or refactor those global overrides into a supported syntax (wrap in
:global(...) only where your CSS module tooling requires it) to satisfy the
linter.
🧹 Nitpick comments (6)
apps/frontend/src/pages/home/index.tsx (1)

79-81: 建议使用主题 token 替代硬编码颜色

空态图标 stroke 使用硬编码 #666666,在深色主题下可能与背景对比度不足。建议改用 currentColor 并通过外层样式控制,或使用全局 CSS 变量(如 var(--color-text-secondary)),与本 PR 引入的主题化体系保持一致。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend/src/pages/home/index.tsx` around lines 79 - 81, The three SVG
path elements in apps/frontend/src/pages/home/index.tsx currently hardcode
stroke="#666666"; replace those stroke attributes with a theme-aware value
(e.g., stroke="currentColor" or stroke="var(--color-text-secondary)") so the
icon follows the app's theming, and ensure the parent element or CSS class sets
the appropriate color token for secondary text; update the path elements (the
ones with d="M4 4H20V16H4V4Z", d="M8 20H16", and d="M12 16V20") accordingly and
verify styling via the container's CSS or theme variables.
apps/backend/src/middleware/jsonReturn.ts (1)

6-9: ctx.res.body 的空值判断存在理论缺陷,建议采用更稳妥的检查方式

当路由通过 ctx.json() 返回响应时,ctx.res.body 会被设置为 ReadableStream,判断生效。但若某个路由显式返回空字符串 ""null 作为合法响应体,由于这两个值在 JavaScript 中为假值,会导致此判断失效并误走到下方 JSON.stringify(data) 分支。

建议改为检查 Content-Type 响应头是否已被设置,或判断 ctx.res 对象是否已被替换,以更准确地区分响应是否已被处理:

// 可选方案 1: 检查 Content-Type 头
if (ctx.res.headers.get('Content-Type')) {
    return;
}

// 可选方案 2: 检查响应体和 Content-Type 的组合
if (ctx.res.body && ctx.res.headers.get('Content-Type')) {
    return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/middleware/jsonReturn.ts` around lines 6 - 9, The current
check in jsonReturn uses a falsy test on ctx.res.body which incorrectly treats
valid empty responses (e.g., "" or null) as not handled; update the guard to
detect whether the response has already been handled by checking the response
headers instead (e.g., use ctx.res.headers.get('Content-Type') or a combined
check of ctx.res.body and ctx.res.headers.get('Content-Type')) so that functions
like jsonReturn or middleware that inspect ctx.res skip formatting only when the
Content-Type (or both body+Content-Type) is present; modify the conditional that
references ctx.res.body to use this header-based check in the jsonReturn
middleware.
apps/backend/src/routes/proxy.ts (1)

40-67: GET detail 与 POST detail action 同名,语义分叉,易踩坑

GET + action=detail 走的是 homeVodHandler(首页 vod 列表),POST + action=detail 才是真正的详情 detailHandler。同一个 action 名字因方法不同映射到完全不同的含义,后续维护/排错成本不低,前端 homeVodApi 又恰好用了 'detail' 作为 action(见 apps/frontend/src/services/vodhub/index.ts)。建议给 homeVod 换一个更直观的 action 名(例如 homeVodhome-vod),前后端一并修改。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/routes/proxy.ts` around lines 40 - 67, The GET action
'detail' currently maps to homeVodHandler while POST 'detail' maps to
detailHandler, causing ambiguous semantics; rename the GET action used for the
homepage VOD list (change the action literal in the proxy GET switch from
'detail' to a clear name like 'homeVod' or 'home-vod') and update the
corresponding handler reference if needed (homeVodHandler), then update the
frontend caller (apps/frontend/src/services/vodhub/index.ts where homeVodApi
sets action='detail') to use the new action string so both backend switch case
(the GET branch) and frontend call match the new name; ensure tests and any
other consumers of action='detail' remain pointed at detailHandler for the POST
path.
apps/frontend/src/pages/category/index.module.scss (1)

56-60: 建议将激活态阴影颜色抽象为 token

.active 里直接写死了 rgba(23, 23, 23, 0.2),与 globals.scss 里统一定义的 --shadow-border / --color-primary-alpha-* 体系不一致,后续切换主题/暗色模式时这条硬编码很容易被漏掉。detail/index.module.scsssetting/index.module.scss 里也有同样的写法,建议在 globals.scss 新增一个 --shadow-border-active(或类似命名)的变量,统一替换。

♻️ 建议改动
         &.active {
             color: var(--color-primary);
             background: var(--color-primary-alpha-low);
-            box-shadow: 0px 0px 0px 1px rgba(23, 23, 23, 0.2);
+            box-shadow: var(--shadow-border-active);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend/src/pages/category/index.module.scss` around lines 56 - 60, The
.active rule hardcodes box-shadow color rgba(23,23,23,0.2); add a new CSS token
(e.g. --shadow-border-active) to globals.scss and replace the hardcoded rgba in
apps/frontend/src/pages/category/index.module.scss (selector &.active) with
var(--shadow-border-active); do the same replacement in detail/index.module.scss
and setting/index.module.scss to keep shadow colors consistent across
themes/dark mode.
apps/frontend/src/lib/store/useSettingStore.ts (1)

8-17: updateSetting 强制传全量字段,调用点存在冗余兜底

updateSetting: (setting: { vod_hub_api: string; site_name: string; current_site: string }) => void 要求每次调用都传完整三个字段,导致 cms-management/index.tsx 里出现 vod_hub_api: vod_hub_api || ''site_name: site_name || '' 这种模板式重复。既然下面直接 set(setting) 做 shallow merge,签名完全可以接受 Partial<…>,调用侧只传要变更的字段,既更符合 zustand 的惯用法,也少一层状态不一致风险(比如误把别的字段覆盖成空串)。

♻️ 建议改动
-    updateSetting: (setting: { vod_hub_api: string; site_name: string; current_site: string }) => void;
+    updateSetting: (setting: Partial<Pick<SettingStore, 'vod_hub_api' | 'site_name' | 'current_site'>>) => void;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend/src/lib/store/useSettingStore.ts` around lines 8 - 17, Change
the updateSetting signature to accept Partial fields instead of requiring all
three keys and ensure it performs a proper merge rather than replacing state;
update the type for updateSetting to (setting: Partial<{ vod_hub_api: string;
site_name: string; current_site: string }>) => void and modify the
implementation in useSettingStore (inside the persist callback where
updateSetting currently calls set(setting)) to merge incoming partials into
existing state (e.g., via set(prev => ({ ...prev, ...setting })) so callers like
cms-management/index.tsx can pass only changed fields.
apps/frontend/src/components/video/VodList/index.module.scss (1)

11-32: 收窄 transition,并移除无效的 border-color。

Line 12 的 transition: all 会把非预期属性也纳入动画;Line 31 没有对应 border,当前不会生效。

建议调整
 .vod-list-item-cover {
-    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+    transition:
+        transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
+        box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@@
 .vod-list-item-cover:hover {
     transform: translateY(-4px);
     box-shadow: var(--shadow-card);
-    border-color: transparent;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend/src/components/video/VodList/index.module.scss` around lines 11
- 32, The .vod-list-item-cover rule uses a broad "transition: all" which
animates unintended properties; replace it with specific properties to animate
(e.g., transition: transform 0.3s cubic-bezier(0.4,0,0.2,1), box-shadow 0.3s
cubic-bezier(0.4,0,0.2,1)) and keep the image-specific transform transition on
.vod-list-item-cover img; also remove the ineffective border-color declaration
from .vod-list-item-cover:hover since there is no border to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/backend/src/routes/proxy.ts`:
- Around line 39-69: The current branch treats any non-GET method as POST which
wrongly routes PUT/DELETE/PATCH/OPTIONS into handlers like detailHandler,
playHandler, searchHandler, categoryHandler; change the conditional to
explicitly check if method === 'POST' and run those POST handlers only in that
branch, and for any other method return a "Method Not Allowed" response (e.g.,
set ctx.status = 405 and ctx.json({ code: -1, message: 'Method Not Allowed',
data: [] })) so unexpected methods don't reach ctx.req.json() or the POST
handlers.
- Around line 20-35: The x-proxy-target header is untrusted and leads to SSRF
when used to build namespace.url for downstream handlers (createNamespace ->
namespace.url -> homeHandler/detailHandler/searchHandler); fix by validating and
sanitizing target early: parse the header as a URL and reject if protocol is not
http/https, reject hostnames that resolve to loopback/private/link-local IP
ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16,
::1, fc00::/7), or alternatively validate against a server-side CMS allowlist
(useCmsStore-sourced whitelist) and return a 400/403 when invalid; consider
changing the API to accept only a CMS id and resolve the URL server-side instead
of trusting x-proxy-target.

In `@apps/frontend/src/components/BasicLayout.module.scss`:
- Line 2: The font-family declaration in BasicLayout.module.scss uses quoted
single-word family names ('Geist', 'Inter') which triggers stylelint rule
font-family-name-quotes; remove the quotes around Geist and Inter so the
declaration becomes font-family: Geist, Inter, 'Microsoft YaHei', sans-serif;
and keep quotes only for multi-word families like "Microsoft YaHei" to satisfy
the linter.

In `@apps/frontend/src/components/cms-management/index.tsx`:
- Around line 167-173: The icon-only action buttons in the render function (the
Buttons using EyeOutlined, EditOutlined, and DeleteOutlined) lack accessible
names; update the three Buttons inside that render (the ones that call
handleView(record), handleEdit(record), and the Delete Button inside Popconfirm
that triggers handleDelete(record.id)) to include aria-label attributes (e.g.,
aria-label="View CMS", "Edit CMS", "Delete CMS") and optionally add title
attributes with the same text to provide screen-reader and tooltip support.
- Around line 23-32: The useEffect hooks call fetchCmsList and reference
error/clearError but lack proper dependencies and do not handle async
rejections; update the first effect to either include fetchCmsList in its
dependency array or memoize fetchCmsList (useCallback) and invoke it via an
async IIFE or named async function that catches and handles errors to avoid
unhandled promise rejections; for the second effect, include clearError in the
dependency array (or memoize it) so the effect signature is stable; ensure you
call message.error(error) only after verifying error is defined and clearError
is invoked in the same controlled flow.

In `@apps/frontend/src/globals.scss`:
- Around line 2-4: 在 globals.scss 的 `@font-face` 规则中修复 stylelint 报错:将单词字体名的
font-family 值去掉引号(例如把 "Geist" 改为 Geist)——在涉及的 font-family 属性(出现在当前 `@font-face`
块和文件中另一个相同位置引用,原评论提到的行 3 与 63)移除引号;对于包含空格的字体名仍保留引号,保持其它 `@font-face` 配置不变。

In `@apps/frontend/src/lib/store/useThemeStore.ts`:
- Line 15: Persisted themeId may be an outdated value (e.g., "midnight") not
present in the new ThemeId union; update the store (useThemeStore) to validate
and normalize persisted values during rehydration by adding a migrate/version
step or using onRehydrateStorage to check themeId against the allowed ThemeId
set (the ThemeId type and getTheme helper) and replace any invalid value with
the default 'vercel' so the store.themeId, theme selector UI, and ConfigProvider
key={themeId} always receive a valid theme id.

In `@apps/frontend/src/lib/store/useVodSitesStore.ts`:
- Around line 43-52: The getVodTypes function in useVodSitesStore synchronously
reads cmsList via useCmsStore.getState() which can be empty before persist
hydration; update either the useCmsStore persist config to expose/handle
hydration (e.g., add hasHydrated or onRehydrateStorage hooks in the persist
options so store consumers can await/observe hydration) or change
useVodSitesStore.getVodTypes to wait for persistence hydration before calling
useCmsStore.getState() (use the exposed hasHydrated() or an
onRehydrateStorage-based promise) so the enabled Cms entries are reliably
present when building newSites; ensure InitProvider which calls getVodTypes uses
the same hydration-aware path.

In `@apps/frontend/src/lib/themes/index.ts`:
- Around line 210-214: Button's current color pair (Button.colorPrimary:
'#ff385c' with Button.primaryColor: '#ffffff') fails WCAG AA contrast for
normal-size text; update the theme to either use a deeper button background
(replace Button.colorPrimary with a darker hex) or set Button.primaryColor to a
dark text color when keeping a light background. Locate the Button object in
this file (symbols: Button, colorPrimary, primaryColor) and apply the change
consistently for the other affected theme blocks noted (also around the other
occurrence at the 332-336 region) so all emphasized/primary button variants meet
at least 4.5:1 contrast.

In `@apps/frontend/src/pages/setting/index.module.scss`:
- Around line 60-128: The top-level :global(...) selectors (e.g.
:global(.ant-input), :global(.ant-select-selector), :global(.ant-btn),
:global(.ant-card), :global(.ant-form-item-label)) are currently global and will
leak these !important antd overrides app-wide; scope them by nesting them under
the Setting page/container class (e.g. wrap them inside .setting-page { ... } or
.setting-card { ... } so selectors become local to the settings page), or move
the intended cross-app antd overrides into the global overrides stylesheet
(globals.scss / antd-overrides.scss) instead; update the CSS module so only the
chosen approach is applied and remove top-level :global rules that are not meant
to be global.

In `@apps/frontend/src/pages/setting/index.tsx`:
- Around line 93-100: The Reset button currently only calls form.resetFields(),
which reverts form UI but does not update the persisted state in
useSettingStore, causing current_site to be restored from the store on next
render; change the Button onClick to also call the store updater (e.g.,
updateSetting) to clear the persistent fields (for example vod_hub_api,
site_name, current_site) or otherwise align with product intent (or rename the
button to "重置当前编辑" if only UI reset is desired) so that form.resetFields() and
useSettingStore remain consistent.
- Around line 51-69: handleApiChange updates the persisted setting before
verifying the API, so a failed getVodTypes leaves vod_hub_api saved to a bad
value; fix by either (A) capture the original vod_hub_api (from
form.getFieldValue('vod_hub_api') or the store) and on catch call updateSetting
to restore it and reset form.setFieldValue('current_site', original) and
re-populate vod types, or (B) prefer changing the order in handleApiChange: call
getVodTypes() (and clearVodTypes() if needed) and only on success call
updateSetting({ vod_hub_api: apiValue, site_name, current_site: '' }) and
form.setFieldValue('current_site',''), so the store is only mutated when
verification via getVodTypes succeeds; use the existing functions updateSetting,
getVodTypes, clearVodTypes, and form.setFieldValue to implement the chosen
approach.
- Around line 125-238: The current bug is two <Form form={form}> components
sharing the same useForm instance (form), which breaks field state and
validation; fix by either (A) creating a separate form via useForm() for the
site-selection form and merging values in your submit handler (reference
useForm, getFieldsValue, updateSetting, handleApiChange) or (B) moving the
Form.Item name="current_site" into the existing primary Form (the one with
site_name/vod_hub_api) and remove the second Form wrapper so only one form
instance (form) is bound; implement whichever option keeps CmsManagement as a
visual Card only and ensure handleSubmit/ updateSetting reads both field values
accordingly.

In `@apps/frontend/src/services/vodhub/index.ts`:
- Around line 86-103: The proxy branch in categoryApi currently only forwards {
id, page } to request.post when cmsUrl is present, dropping limit and filters
(e.g., filters.area/ lang/ year) and causing inconsistent behavior versus the
non-proxy branch; update the cmsUrl branch (inside categoryApi) to forward the
full data object (or explicitly include data.limit and data.filters) in the
request.post body so the proxy `/proxy` call receives the same params as the
non-proxy `/${site_name}/category` call, and if the custom CMS truly doesn’t
support these fields instead add a clear comment or adjust types to indicate
they are ignored.
- Around line 18-25: The getCmsUrl function currently returns undefined for site
values starting with "custom_" when getCmsById(id) returns no CMS, causing
callers to fall back to backend routes; update getCmsUrl (in
apps/frontend/src/services/vodhub/index.ts) to explicitly handle missing custom
CMS by either throwing a descriptive error (e.g. "Custom CMS not found for
custom_<id>") or returning a clear sentinel (e.g. null/empty object) that
callers can check, and ensure callers of getCmsUrl (and any code that builds
routes from site names) are updated to catch the error or handle the sentinel so
no request is sent to a non-existent /custom_xxx/* backend namespace.

In `@CLAUDE.MD`:
- Around line 61-63: The documentation claims the proxy route supports ALL
methods for `/api/vodhub/proxy` but the implementation only accepts `GET |
POST`; update either the doc or the router so they match: either change the
CLAUDE.MD description to state `GET | POST /api/vodhub/proxy` (and update other
mentions at the same block around the CMS Management & Proxy section) or modify
the route handler/route generator for `/api/vodhub/proxy` to accept all HTTP
verbs (e.g., map other methods through the same proxy logic) so the
documentation remains accurate.
- Around line 129-133: The Theme System documentation in CLAUDE.MD still lists
the old built-in themes (midnight / aurora / cyber); update the "### Theme
System" section to match the current implementation in lib/themes/index.ts (now
exporting vercel, airbnb, claude) by replacing the three bullet items with the
new theme names and brief descriptions (e.g., light/dark and accent color)
consistent with the theme definitions in apps/frontend/src/lib/themes/index.ts
(verify symbol names vercel, airbnb, claude).

In `@packages/shared/package.json`:
- Around line 10-11: The package export maps expose "./types":
"./src/types/index.ts" but src/types/index.ts does not re-export the custom-cms
types found in src/types/custom-cms.ts, so add a re-export in src/types/index.ts
(e.g., export * from "./custom-cms") to allow consumers to import from
`@vodhub/shared/types`; update or verify consumers like cms-management and
useCmsStore still work after adding the re-export, or if you prefer the
subpath-only design, document and keep using `@vodhub/shared/types/custom-cms`
consistently instead of changing index.ts.

---

Outside diff comments:
In `@apps/frontend/src/pages/setting/index.module.scss`:
- Around line 33-128: The stylesheet uses quoted generic font-family names and
unrecognized :global pseudo-class usages causing Stylelint errors; remove quotes
from font-family entries (update occurrences in the rules containing
"font-family: 'Geist', 'Inter', sans-serif;" — e.g., inside the top-level block,
.setting-form-content, :global(.ant-form-item-label) label, :global(.ant-btn),
and :global(.ant-card)) and either add ":global" to the
selector-pseudo-class-no-unknown ignorePseudoClasses in your .stylelintrc (so
rules like :global(.ant-input) are accepted) or refactor those global overrides
into a supported syntax (wrap in :global(...) only where your CSS module tooling
requires it) to satisfy the linter.

---

Nitpick comments:
In `@apps/backend/src/middleware/jsonReturn.ts`:
- Around line 6-9: The current check in jsonReturn uses a falsy test on
ctx.res.body which incorrectly treats valid empty responses (e.g., "" or null)
as not handled; update the guard to detect whether the response has already been
handled by checking the response headers instead (e.g., use
ctx.res.headers.get('Content-Type') or a combined check of ctx.res.body and
ctx.res.headers.get('Content-Type')) so that functions like jsonReturn or
middleware that inspect ctx.res skip formatting only when the Content-Type (or
both body+Content-Type) is present; modify the conditional that references
ctx.res.body to use this header-based check in the jsonReturn middleware.

In `@apps/backend/src/routes/proxy.ts`:
- Around line 40-67: The GET action 'detail' currently maps to homeVodHandler
while POST 'detail' maps to detailHandler, causing ambiguous semantics; rename
the GET action used for the homepage VOD list (change the action literal in the
proxy GET switch from 'detail' to a clear name like 'homeVod' or 'home-vod') and
update the corresponding handler reference if needed (homeVodHandler), then
update the frontend caller (apps/frontend/src/services/vodhub/index.ts where
homeVodApi sets action='detail') to use the new action string so both backend
switch case (the GET branch) and frontend call match the new name; ensure tests
and any other consumers of action='detail' remain pointed at detailHandler for
the POST path.

In `@apps/frontend/src/components/video/VodList/index.module.scss`:
- Around line 11-32: The .vod-list-item-cover rule uses a broad "transition:
all" which animates unintended properties; replace it with specific properties
to animate (e.g., transition: transform 0.3s cubic-bezier(0.4,0,0.2,1),
box-shadow 0.3s cubic-bezier(0.4,0,0.2,1)) and keep the image-specific transform
transition on .vod-list-item-cover img; also remove the ineffective border-color
declaration from .vod-list-item-cover:hover since there is no border to change.

In `@apps/frontend/src/lib/store/useSettingStore.ts`:
- Around line 8-17: Change the updateSetting signature to accept Partial fields
instead of requiring all three keys and ensure it performs a proper merge rather
than replacing state; update the type for updateSetting to (setting: Partial<{
vod_hub_api: string; site_name: string; current_site: string }>) => void and
modify the implementation in useSettingStore (inside the persist callback where
updateSetting currently calls set(setting)) to merge incoming partials into
existing state (e.g., via set(prev => ({ ...prev, ...setting })) so callers like
cms-management/index.tsx can pass only changed fields.

In `@apps/frontend/src/pages/category/index.module.scss`:
- Around line 56-60: The .active rule hardcodes box-shadow color
rgba(23,23,23,0.2); add a new CSS token (e.g. --shadow-border-active) to
globals.scss and replace the hardcoded rgba in
apps/frontend/src/pages/category/index.module.scss (selector &.active) with
var(--shadow-border-active); do the same replacement in detail/index.module.scss
and setting/index.module.scss to keep shadow colors consistent across
themes/dark mode.

In `@apps/frontend/src/pages/home/index.tsx`:
- Around line 79-81: The three SVG path elements in
apps/frontend/src/pages/home/index.tsx currently hardcode stroke="#666666";
replace those stroke attributes with a theme-aware value (e.g.,
stroke="currentColor" or stroke="var(--color-text-secondary)") so the icon
follows the app's theming, and ensure the parent element or CSS class sets the
appropriate color token for secondary text; update the path elements (the ones
with d="M4 4H20V16H4V4Z", d="M8 20H16", and d="M12 16V20") accordingly and
verify styling via the container's CSS or theme variables.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4a0660f6-869b-43ec-bffb-b9d3ec54a90d

📥 Commits

Reviewing files that changed from the base of the PR and between d840f3f and e754164.

⛔ Files ignored due to path filters (7)
  • apps/frontend/docs/images/dark.png is excluded by !**/*.png
  • apps/frontend/docs/images/home.png is excluded by !**/*.png
  • apps/frontend/docs/images/mobile.png is excluded by !**/*.png
  • apps/frontend/docs/images/play.png is excluded by !**/*.png
  • apps/frontend/docs/images/search.png is excluded by !**/*.png
  • apps/frontend/docs/images/theme.png is excluded by !**/*.png
  • apps/frontend/docs/images/theme1.png is excluded by !**/*.png
📒 Files selected for processing (30)
  • CLAUDE.MD
  • README.md
  • apps/backend/src/middleware/jsonReturn.ts
  • apps/backend/src/routes/proxy.ts
  • apps/frontend/src/components/BasicLayout.module.scss
  • apps/frontend/src/components/InitProvider.tsx
  • apps/frontend/src/components/SiteHeader.tsx
  • apps/frontend/src/components/ThemeProvider.tsx
  • apps/frontend/src/components/cms-management/index.tsx
  • apps/frontend/src/components/ui/ThemeSelector.tsx
  • apps/frontend/src/components/video/VodList/index.module.scss
  • apps/frontend/src/components/video/VodList/index.tsx
  • apps/frontend/src/components/video/VodPalyer/index.module.scss
  • apps/frontend/src/components/video/VodSearch/index.tsx
  • apps/frontend/src/components/video/VodTypes/index.module.scss
  • apps/frontend/src/globals.scss
  • apps/frontend/src/lib/store/useCmsStore.ts
  • apps/frontend/src/lib/store/useSettingStore.ts
  • apps/frontend/src/lib/store/useThemeStore.ts
  • apps/frontend/src/lib/store/useVodSitesStore.ts
  • apps/frontend/src/lib/themes/index.ts
  • apps/frontend/src/pages/category/index.module.scss
  • apps/frontend/src/pages/detail/index.module.scss
  • apps/frontend/src/pages/home/index.module.scss
  • apps/frontend/src/pages/home/index.tsx
  • apps/frontend/src/pages/setting/index.module.scss
  • apps/frontend/src/pages/setting/index.tsx
  • apps/frontend/src/services/vodhub/index.ts
  • packages/shared/package.json
  • packages/shared/src/types/custom-cms.ts

Comment on lines +20 to +35
app.all('/', async (ctx: Context): Promise<Response> => {
const target = ctx.req.header('x-proxy-target') || '';
const path = ctx.req.header('x-proxy-path');
const action = ctx.req.header('x-proxy-action') || '';

logger.info(`Proxy request: target=${target}, action=${action}`);

if (!target) {
return ctx.json({ error: 'header x-proxy-target is required' }, 400);
return ctx.json({ code: -1, message: 'header x-proxy-target is required', data: [] });
}
if (!path) {
return ctx.json({ error: 'header x-proxy-path is required' }, 400);
}
if (!isAllowedTarget(target)) {
return ctx.json({ error: 'target not allowed' }, 403 as const);

if (!action) {
return ctx.json({ code: -1, message: 'header x-proxy-action is required', data: [] });
}

const headers = ctx.req.header();
const url = `${target}${path}?${strParams}`;
const namespace = createNamespace(target);
const method = ctx.req.method;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

严重:x-proxy-target 存在 SSRF 风险,需要做目标校验

target 直接来自 HTTP 头,没有任何协议/主机校验就被塞进 namespace.url,并被下游 homeHandler / detailHandler / searchHandler 等拼成 ${namespace.url}/api.php/provide/vod 发出服务器端请求(详见 apps/backend/src/utils/cms/home/index.ts.../detail/index.ts.../search/index.ts)。这意味着任何能调用 /proxy 的客户端都可以让后端去请求内网地址(如 http://127.0.0.1:xxx、云厂商元数据服务 http://169.254.169.254/)并把响应返回给自己,构成典型的 SSRF。

建议至少做以下校验(任选其一并组合):

  • 解析 URL,限制 protocol 只能是 http:/https:;
  • 基于 useCmsStore 中已登记的 CMS 列表做服务端侧校验(例如后端同步一份允许的 host/前缀白名单),拒绝任意目标;
  • 阻断 hostname 指向内网/回环/链路本地地址(127.0.0.0/810.0.0.0/8172.16.0.0/12192.168.0.0/16169.254.0.0/16::1fc00::/7 等);
  • 如确实需要支持用户自定义 CMS,强烈建议在前端请求时只传 CMS id,后端根据受控配置解析 URL,而不是信任来自客户端的任意 URL。
🛡️ 参考改动(仅示意,白名单需按实际部署环境调整)
+const isAllowedTarget = (raw: string): boolean => {
+    try {
+        const u = new URL(raw);
+        if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
+        const host = u.hostname.toLowerCase();
+        // 禁止回环与常见内网段
+        if (host === 'localhost' || host === '0.0.0.0') return false;
+        if (/^127\./.test(host)) return false;
+        if (/^10\./.test(host)) return false;
+        if (/^192\.168\./.test(host)) return false;
+        if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return false;
+        if (/^169\.254\./.test(host)) return false;
+        return true;
+    } catch {
+        return false;
+    }
+};
+
 app.all('/', async (ctx: Context): Promise<Response> => {
     const target = ctx.req.header('x-proxy-target') || '';
     const action = ctx.req.header('x-proxy-action') || '';
@@
     if (!target) {
         return ctx.json({ code: -1, message: 'header x-proxy-target is required', data: [] });
     }
+    if (!isAllowedTarget(target)) {
+        return ctx.json({ code: -1, message: 'invalid x-proxy-target', data: [] });
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.all('/', async (ctx: Context): Promise<Response> => {
const target = ctx.req.header('x-proxy-target') || '';
const path = ctx.req.header('x-proxy-path');
const action = ctx.req.header('x-proxy-action') || '';
logger.info(`Proxy request: target=${target}, action=${action}`);
if (!target) {
return ctx.json({ error: 'header x-proxy-target is required' }, 400);
return ctx.json({ code: -1, message: 'header x-proxy-target is required', data: [] });
}
if (!path) {
return ctx.json({ error: 'header x-proxy-path is required' }, 400);
}
if (!isAllowedTarget(target)) {
return ctx.json({ error: 'target not allowed' }, 403 as const);
if (!action) {
return ctx.json({ code: -1, message: 'header x-proxy-action is required', data: [] });
}
const headers = ctx.req.header();
const url = `${target}${path}?${strParams}`;
const namespace = createNamespace(target);
const method = ctx.req.method;
const isAllowedTarget = (raw: string): boolean => {
try {
const u = new URL(raw);
if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
const host = u.hostname.toLowerCase();
// 禁止回环与常见内网段
if (host === 'localhost' || host === '0.0.0.0') return false;
if (/^127\./.test(host)) return false;
if (/^10\./.test(host)) return false;
if (/^192\.168\./.test(host)) return false;
if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return false;
if (/^169\.254\./.test(host)) return false;
return true;
} catch {
return false;
}
};
app.all('/', async (ctx: Context): Promise<Response> => {
const target = ctx.req.header('x-proxy-target') || '';
const action = ctx.req.header('x-proxy-action') || '';
logger.info(`Proxy request: target=${target}, action=${action}`);
if (!target) {
return ctx.json({ code: -1, message: 'header x-proxy-target is required', data: [] });
}
if (!isAllowedTarget(target)) {
return ctx.json({ code: -1, message: 'invalid x-proxy-target', data: [] });
}
if (!action) {
return ctx.json({ code: -1, message: 'header x-proxy-action is required', data: [] });
}
const namespace = createNamespace(target);
const method = ctx.req.method;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/routes/proxy.ts` around lines 20 - 35, The x-proxy-target
header is untrusted and leads to SSRF when used to build namespace.url for
downstream handlers (createNamespace -> namespace.url ->
homeHandler/detailHandler/searchHandler); fix by validating and sanitizing
target early: parse the header as a URL and reject if protocol is not
http/https, reject hostnames that resolve to loopback/private/link-local IP
ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16,
::1, fc00::/7), or alternatively validate against a server-side CMS allowlist
(useCmsStore-sourced whitelist) and return a 400/403 when invalid; consider
changing the API to accept only a CMS id and resolve the URL server-side instead
of trusting x-proxy-target.

Comment on lines +39 to 69
if (method === 'GET') {
switch (action) {
case 'list':
result = ctx.json(await homeHandler(ctx, namespace));
break;
case 'detail':
result = ctx.json(await homeVodHandler(ctx, namespace));
break;
default:
result = ctx.json({ code: -1, message: 'Unknown GET action: ' + action, data: [] });
break;
}
} else {
switch (action) {
case 'detail':
result = ctx.json(await detailHandler(ctx, namespace));
break;
case 'play':
result = ctx.json(await playHandler(ctx, namespace));
break;
case 'search':
result = ctx.json(await searchHandler(ctx, namespace));
break;
case 'category':
result = ctx.json(await categoryHandler(ctx, namespace));
break;
default:
result = ctx.json({ code: -1, message: 'Unknown POST action: ' + action, data: [] });
break;
}

const { status, statusText, headers: resHeaders } = response;
return new Response(response.body, { status, statusText, headers: resHeaders });
} catch (error) {
logger.error(`Proxy request failed: ${error instanceof Error ? error.message : String(error)}`);
return ctx.json({ error: 'Proxy request failed' }, 500);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

非 GET 请求的分支会把 PUT/DELETE/PATCH 当成 POST 处理

else 分支覆盖了所有非 GET 的方法,但前端只用 GET/POST(见 apps/frontend/src/services/vodhub/index.ts),PUT/DELETE/OPTIONS 等会意外命中 POST 分支的 detail/play/search/category handler,下游 ctx.req.json() 失败时只能靠 catch 兜底,行为并不明确。建议显式判断 POST,其它方法统一返回 Method Not Allowed

♻️ 建议改动
-    if (method === 'GET') {
+    if (method === 'GET') {
         switch (action) {
             ...
         }
-    } else {
+    } else if (method === 'POST') {
         switch (action) {
             ...
         }
+    } else {
+        result = ctx.json({ code: -1, message: `Method ${method} not allowed`, data: [] });
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (method === 'GET') {
switch (action) {
case 'list':
result = ctx.json(await homeHandler(ctx, namespace));
break;
case 'detail':
result = ctx.json(await homeVodHandler(ctx, namespace));
break;
default:
result = ctx.json({ code: -1, message: 'Unknown GET action: ' + action, data: [] });
break;
}
} else {
switch (action) {
case 'detail':
result = ctx.json(await detailHandler(ctx, namespace));
break;
case 'play':
result = ctx.json(await playHandler(ctx, namespace));
break;
case 'search':
result = ctx.json(await searchHandler(ctx, namespace));
break;
case 'category':
result = ctx.json(await categoryHandler(ctx, namespace));
break;
default:
result = ctx.json({ code: -1, message: 'Unknown POST action: ' + action, data: [] });
break;
}
const { status, statusText, headers: resHeaders } = response;
return new Response(response.body, { status, statusText, headers: resHeaders });
} catch (error) {
logger.error(`Proxy request failed: ${error instanceof Error ? error.message : String(error)}`);
return ctx.json({ error: 'Proxy request failed' }, 500);
}
if (method === 'GET') {
switch (action) {
case 'list':
result = ctx.json(await homeHandler(ctx, namespace));
break;
case 'detail':
result = ctx.json(await homeVodHandler(ctx, namespace));
break;
default:
result = ctx.json({ code: -1, message: 'Unknown GET action: ' + action, data: [] });
break;
}
} else if (method === 'POST') {
switch (action) {
case 'detail':
result = ctx.json(await detailHandler(ctx, namespace));
break;
case 'play':
result = ctx.json(await playHandler(ctx, namespace));
break;
case 'search':
result = ctx.json(await searchHandler(ctx, namespace));
break;
case 'category':
result = ctx.json(await categoryHandler(ctx, namespace));
break;
default:
result = ctx.json({ code: -1, message: 'Unknown POST action: ' + action, data: [] });
break;
}
} else {
result = ctx.json({ code: -1, message: `Method ${method} not allowed`, data: [] });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/routes/proxy.ts` around lines 39 - 69, The current branch
treats any non-GET method as POST which wrongly routes PUT/DELETE/PATCH/OPTIONS
into handlers like detailHandler, playHandler, searchHandler, categoryHandler;
change the conditional to explicitly check if method === 'POST' and run those
POST handlers only in that branch, and for any other method return a "Method Not
Allowed" response (e.g., set ctx.status = 405 and ctx.json({ code: -1, message:
'Method Not Allowed', data: [] })) so unexpected methods don't reach
ctx.req.json() or the POST handlers.

@@ -1,5 +1,6 @@
.vod-layout {
font-family: 'Righteous', 'Poppins', 'Microsoft YaHei', sans-serif;
font-family: 'Geist', 'Inter', 'Microsoft YaHei', sans-serif;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Stylelint font-family-name-quotes 报错。

GeistInter 是单字符串标识符,按仓库的 stylelint 规则不应加引号。建议去掉引号以通过 lint。

♻️ 建议修改
-    font-family: 'Geist', 'Inter', 'Microsoft YaHei', sans-serif;
+    font-family: Geist, Inter, 'Microsoft YaHei', sans-serif;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
font-family: 'Geist', 'Inter', 'Microsoft YaHei', sans-serif;
font-family: Geist, Inter, 'Microsoft YaHei', sans-serif;
🧰 Tools
🪛 Stylelint (17.7.0)

[error] 2-2: Expected no quotes around "Geist" (font-family-name-quotes)

(font-family-name-quotes)


[error] 2-2: Expected no quotes around "Inter" (font-family-name-quotes)

(font-family-name-quotes)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend/src/components/BasicLayout.module.scss` at line 2, The
font-family declaration in BasicLayout.module.scss uses quoted single-word
family names ('Geist', 'Inter') which triggers stylelint rule
font-family-name-quotes; remove the quotes around Geist and Inter so the
declaration becomes font-family: Geist, Inter, 'Microsoft YaHei', sans-serif;
and keep quotes only for multi-word families like "Microsoft YaHei" to satisfy
the linter.

Comment on lines +23 to +32
useEffect(() => {
fetchCmsList();
}, []);

useEffect(() => {
if (error) {
message.error(error);
clearError();
}
}, [error]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect React hooks lint configuration and this component's effect usage.
set -euo pipefail

echo "== React hooks lint config =="
fd 'eslint.*|package.json$' --exec sh -c 'echo "--- {}"; rg -n "react-hooks/exhaustive-deps|eslint-plugin-react-hooks" "{}" || true'

echo
echo "== CmsManagement effects =="
rg -n -C3 "useEffect\\(\\(\\) =>" --glob 'index.tsx'

Repository: consistent-k/VodHub

Length of output: 5037


🏁 Script executed:

#!/bin/bash
# 读取完整的 cms-management/index.tsx 文件
echo "=== CmsManagement component ===" 
head -100 apps/frontend/src/components/cms-management/index.tsx

echo -e "\n=== Full file length ==="
wc -l apps/frontend/src/components/cms-management/index.tsx

Repository: consistent-k/VodHub

Length of output: 3491


🏁 Script executed:

#!/bin/bash
# 读取文件的其余部分
echo "=== Rest of CmsManagement (lines 100-265) ==="
sed -n '100,265p' apps/frontend/src/components/cms-management/index.tsx

echo -e "\n=== Find useCmsStore ==="
fd 'useCmsStore' --type f

Repository: consistent-k/VodHub

Length of output: 6302


🏁 Script executed:

#!/bin/bash
# 读取 useCmsStore 实现
cat apps/frontend/src/lib/store/useCmsStore.ts

Repository: consistent-k/VodHub

Length of output: 4925


🏁 Script executed:

#!/bin/bash
# 检查 eslint 配置中 react-hooks 规则的详细设置
echo "=== ESLint config content ==="
head -50 eslint.config.mjs

echo -e "\n=== Checking react-hooks rules ==="
cat eslint.config.mjs | grep -A 20 "reactHooks"

Repository: consistent-k/VodHub

Length of output: 3464


🏁 Script executed:

#!/bin/bash
# 验证这两个 effect 在当前代码中是否真的会触发 ESLint 警告
echo "=== Checking for exhaustive-deps violations in component ==="
rg -A5 'useEffect' apps/frontend/src/components/cms-management/index.tsx | head -40

echo -e "\n=== Checking zustand store export ==="
tail -5 apps/frontend/src/lib/store/useCmsStore.ts

Repository: consistent-k/VodHub

Length of output: 624


补齐 effect 依赖并显式处理异步调用。

第 23-32 行会触发 react-hooks/exhaustive-deps 规则警告。fetchCmsList 抛出错误未被捕获,可能导致未处理的 rejection。

建议调整
 useEffect(() => {
-    fetchCmsList();
-}, []);
+    void fetchCmsList().catch(() => {
+        // 错误状态已由 store 写入 error
+    });
+}, [fetchCmsList]);

 useEffect(() => {
     if (error) {
         message.error(error);
         clearError();
     }
-}, [error]);
+}, [error, clearError]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
fetchCmsList();
}, []);
useEffect(() => {
if (error) {
message.error(error);
clearError();
}
}, [error]);
useEffect(() => {
void fetchCmsList().catch(() => {
// 错误状态已由 store 写入 error
});
}, [fetchCmsList]);
useEffect(() => {
if (error) {
message.error(error);
clearError();
}
}, [error, clearError]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend/src/components/cms-management/index.tsx` around lines 23 - 32,
The useEffect hooks call fetchCmsList and reference error/clearError but lack
proper dependencies and do not handle async rejections; update the first effect
to either include fetchCmsList in its dependency array or memoize fetchCmsList
(useCallback) and invoke it via an async IIFE or named async function that
catches and handles errors to avoid unhandled promise rejections; for the second
effect, include clearError in the dependency array (or memoize it) so the effect
signature is stable; ensure you call message.error(error) only after verifying
error is defined and clearError is invoked in the same controlled flow.

Comment on lines +167 to +173
render: (_: any, record: CustomCms) => (
<Space size="small">
<Button type="text" size="small" icon={<EyeOutlined />} onClick={() => handleView(record)} />
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
<Popconfirm title="确定要删除这个CMS地址吗?" onConfirm={() => handleDelete(record.id)} okText="确定" cancelText="取消">
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

为图标按钮添加可访问名称。

Line 169-172 的查看/编辑/删除按钮只有图标,屏幕阅读器会读成无语义按钮。请加 aria-label(可同时加 title)。

建议调整
-<Button type="text" size="small" icon={<EyeOutlined />} onClick={() => handleView(record)} />
-<Button type="text" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
+<Button type="text" size="small" aria-label="查看CMS详情" title="查看CMS详情" icon={<EyeOutlined />} onClick={() => handleView(record)} />
+<Button type="text" size="small" aria-label="编辑CMS地址" title="编辑CMS地址" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
 <Popconfirm title="确定要删除这个CMS地址吗?" onConfirm={() => handleDelete(record.id)} okText="确定" cancelText="取消">
-    <Button type="text" size="small" danger icon={<DeleteOutlined />} />
+    <Button type="text" size="small" danger aria-label="删除CMS地址" title="删除CMS地址" icon={<DeleteOutlined />} />
 </Popconfirm>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
render: (_: any, record: CustomCms) => (
<Space size="small">
<Button type="text" size="small" icon={<EyeOutlined />} onClick={() => handleView(record)} />
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
<Popconfirm title="确定要删除这个CMS地址吗?" onConfirm={() => handleDelete(record.id)} okText="确定" cancelText="取消">
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
</Popconfirm>
render: (_: any, record: CustomCms) => (
<Space size="small">
<Button type="text" size="small" aria-label="查看CMS详情" title="查看CMS详情" icon={<EyeOutlined />} onClick={() => handleView(record)} />
<Button type="text" size="small" aria-label="编辑CMS地址" title="编辑CMS地址" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
<Popconfirm title="确定要删除这个CMS地址吗?" onConfirm={() => handleDelete(record.id)} okText="确定" cancelText="取消">
<Button type="text" size="small" danger aria-label="删除CMS地址" title="删除CMS地址" icon={<DeleteOutlined />} />
</Popconfirm>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend/src/components/cms-management/index.tsx` around lines 167 -
173, The icon-only action buttons in the render function (the Buttons using
EyeOutlined, EditOutlined, and DeleteOutlined) lack accessible names; update the
three Buttons inside that render (the ones that call handleView(record),
handleEdit(record), and the Delete Button inside Popconfirm that triggers
handleDelete(record.id)) to include aria-label attributes (e.g.,
aria-label="View CMS", "Edit CMS", "Delete CMS") and optionally add title
attributes with the same text to provide screen-reader and tooltip support.

Comment on lines +18 to +25
const getCmsUrl = (site: string): string | undefined => {
if (site.startsWith('custom_')) {
const id = site.replace('custom_', '');
const cms = getCmsById(id);
return cms?.url;
}
return undefined;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

custom_xxx 站点失效时会回落到无效的后端路由。

site_name 形如 custom_xxx 但对应 CMS 已被删除(或 cmsList 尚未完成 hydration)时,getCmsUrl 返回 undefined,随后所有 API 都会走 /${site_name}/... 这条 fallback 分支,实际请求会打到 /custom_xxx/home 这种后端不存在的 namespace,返回 404 或错误而非明确的"站点不可用"提示。建议在 custom_ 前缀但查不到 CMS 时显式抛错或返回空结构,避免误发后端请求。

♻️ 建议修改
 const getCmsUrl = (site: string): string | undefined => {
     if (site.startsWith('custom_')) {
         const id = site.replace('custom_', '');
         const cms = getCmsById(id);
-        return cms?.url;
+        if (!cms?.url) {
+            throw new Error(`Custom CMS not found or disabled: ${id}`);
+        }
+        return cms.url;
     }
     return undefined;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getCmsUrl = (site: string): string | undefined => {
if (site.startsWith('custom_')) {
const id = site.replace('custom_', '');
const cms = getCmsById(id);
return cms?.url;
}
return undefined;
};
const getCmsUrl = (site: string): string | undefined => {
if (site.startsWith('custom_')) {
const id = site.replace('custom_', '');
const cms = getCmsById(id);
if (!cms?.url) {
throw new Error(`Custom CMS not found or disabled: ${id}`);
}
return cms.url;
}
return undefined;
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend/src/services/vodhub/index.ts` around lines 18 - 25, The
getCmsUrl function currently returns undefined for site values starting with
"custom_" when getCmsById(id) returns no CMS, causing callers to fall back to
backend routes; update getCmsUrl (in apps/frontend/src/services/vodhub/index.ts)
to explicitly handle missing custom CMS by either throwing a descriptive error
(e.g. "Custom CMS not found for custom_<id>") or returning a clear sentinel
(e.g. null/empty object) that callers can check, and ensure callers of getCmsUrl
(and any code that builds routes from site names) are updated to catch the error
or handle the sentinel so no request is sent to a non-existent /custom_xxx/*
backend namespace.

Comment on lines 86 to 103
export const categoryApi = (site_name: string, data: CategoryParams) => {
const cmsUrl = getCmsUrl(site_name);
if (cmsUrl) {
return request.post<ApiResponse<CategoryVodData[]>>(`/proxy`, {
headers: {
'x-proxy-target': cmsUrl,
'x-proxy-action': 'category'
},
data: {
id: data.id,
page: data.page
}
});
}
return request.post<VodHubResponse<CategoryVodData[]>>(`/${site_name}/category`, {
data
});
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

代理分支丢失 limitfilters 参数,与非代理分支行为不一致。

非代理分支把完整的 data(含 limitfilters.area/lang/year)透传给后端,而代理分支只转发 { id, page }。如果自定义 CMS 的 category handler 也支持按地区/语言/年份/分页大小筛选,这里会静默丢弃这些过滤器导致筛选失效;如果 handler 的确不支持,建议在类型或注释中明确说明。

♻️ 建议修改
-            data: {
-                id: data.id,
-                page: data.page
-            }
+            data
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/frontend/src/services/vodhub/index.ts` around lines 86 - 103, The proxy
branch in categoryApi currently only forwards { id, page } to request.post when
cmsUrl is present, dropping limit and filters (e.g., filters.area/ lang/ year)
and causing inconsistent behavior versus the non-proxy branch; update the cmsUrl
branch (inside categoryApi) to forward the full data object (or explicitly
include data.limit and data.filters) in the request.post body so the proxy
`/proxy` call receives the same params as the non-proxy `/${site_name}/category`
call, and if the custom CMS truly doesn’t support these fields instead add a
clear comment or adjust types to indicate they are ignored.

Comment thread CLAUDE.MD
Comment on lines +61 to +63
### CMS Management & Proxy
- **Proxy API**: `ALL /api/vodhub/proxy` – forwards requests to user‑defined CMS URLs using `x-proxy-target` and `x-proxy-action` headers to translate actions to the target CMS's API format
- **Frontend store**: CMS configurations are managed locally via `useCmsStore` (Zustand + localStorage) for immediate UI updates; backend API synchronization may be added in the future.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

对齐 proxy 路由方法约束。

Line 62 记录 proxy 是 ALL /api/vodhub/proxy,但 Line 114 只允许 GET | POST。如果路由类型/生成器需要表达 proxy,这里会把维护者引到错误的 route shape。

建议调整
-- `method?`: 'GET' | 'POST' – defaults to GET
+- `method?`: 'GET' | 'POST' | 'ALL' – defaults to GET

Also applies to: 107-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.MD` around lines 61 - 63, The documentation claims the proxy route
supports ALL methods for `/api/vodhub/proxy` but the implementation only accepts
`GET | POST`; update either the doc or the router so they match: either change
the CLAUDE.MD description to state `GET | POST /api/vodhub/proxy` (and update
other mentions at the same block around the CMS Management & Proxy section) or
modify the route handler/route generator for `/api/vodhub/proxy` to accept all
HTTP verbs (e.g., map other methods through the same proxy logic) so the
documentation remains accurate.

Comment thread CLAUDE.MD
Comment on lines 129 to +133
### Theme System
Three built-in themes defined in `lib/themes/index.ts`:
- **midnight**: Dark with red accent
- **aurora**: Light with cyan accent
- **cyber**: Dark with purple accent
Three builtin themes defined in `lib/themes/index.ts`:
- **midnight**: dark with red accent
- **aurora**: light with cyan accent
- **cyber**: dark with purple accent
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

更新内置主题列表,避免文档与实现不一致。

Line 130-133 仍写的是旧的 midnight / aurora / cyber,但当前 apps/frontend/src/lib/themes/index.ts 已改为 vercel / airbnb / claude

建议调整
-Three built‑in themes defined in `lib/themes/index.ts`:
-- **midnight**: dark with red accent
-- **aurora**: light with cyan accent  
-- **cyber**: dark with purple accent
+Three built‑in themes defined in `lib/themes/index.ts`:
+- **vercel**: light with black primary and blue accent
+- **airbnb**: light with Rausch red accent
+- **claude**: warm neutral palette with terracotta accent
🧰 Tools
🪛 LanguageTool

[grammar] ~130-~130: Ensure spelling is correct
Context: ...i‑theme support ### Theme System Three built‑in themes defined in lib/themes/index.ts: - **m...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.MD` around lines 129 - 133, The Theme System documentation in
CLAUDE.MD still lists the old built-in themes (midnight / aurora / cyber);
update the "### Theme System" section to match the current implementation in
lib/themes/index.ts (now exporting vercel, airbnb, claude) by replacing the
three bullet items with the new theme names and brief descriptions (e.g.,
light/dark and accent color) consistent with the theme definitions in
apps/frontend/src/lib/themes/index.ts (verify symbol names vercel, airbnb,
claude).

Comment on lines +10 to +11
"./types": "./src/types/index.ts",
"./types/custom-cms": "./src/types/custom-cms.ts"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'index.ts' packages/shared/src/types -x cat {}
echo '---'
rg -nP "@vodhub/shared/types(/custom-cms)?" -C1

Repository: consistent-k/VodHub

Length of output: 2472


确认 ./src/types/index.ts 未重新导出 custom-cms 类型

检查结果显示 index.ts 中未包含对 custom-cms.ts 的重新导出。当前代码库中所有 custom-cms 相关类型的导入都统一使用子路径 @vodhub/shared/types/custom-cms(如 cms-management 组件和 useCmsStore),暂未发现混用两种导入路径的情况。

建议明确选择其中一种方案:

  • 方案1:在 index.ts 中添加 re-export,允许消费方从 @vodhub/shared/types 统一导入
  • 方案2:保持当前子路径设计,用于模块化隔离
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared/package.json` around lines 10 - 11, The package export maps
expose "./types": "./src/types/index.ts" but src/types/index.ts does not
re-export the custom-cms types found in src/types/custom-cms.ts, so add a
re-export in src/types/index.ts (e.g., export * from "./custom-cms") to allow
consumers to import from `@vodhub/shared/types`; update or verify consumers like
cms-management and useCmsStore still work after adding the re-export, or if you
prefer the subpath-only design, document and keep using
`@vodhub/shared/types/custom-cms` consistently instead of changing index.ts.

@consistent-k consistent-k merged commit c614282 into main Apr 18, 2026
5 checks passed
@consistent-k consistent-k deleted the dev branch April 18, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant