Skip to content

feat(v2.0): 完整版本更新、批量处理、版本历史与各类安全修复 - #7

Merged
one-ea merged 20 commits into
mainfrom
dev
Apr 12, 2026
Merged

feat(v2.0): 完整版本更新、批量处理、版本历史与各类安全修复#7
one-ea merged 20 commits into
mainfrom
dev

Conversation

@one-ea

@one-ea one-ea commented Apr 12, 2026

Copy link
Copy Markdown
Owner

Monolith V2.0 阶段完结与补丁大冲刺

本 PR 涵盖了 Monolith 博客 V2.0 冲刺阶段剩余的所有功能及安全漏洞修复:

🌟 新特性

  1. chore(deps): bump vite from 6.4.2 to 8.0.8 in /client #13 批量操作:管理员能在后台批量发布、撤回和删除文章。
  2. chore(config): 优化 Dependabot 合并策略 (启用群组构建) #14 文章版本历史快照:可随时撤回旧版本,杜绝操作失误丢失关键文本。
  3. fix: address CodeRabbit review findings (竞态优化与样式修复) #16 Service Worker 离线机制:依托 vite-plugin-pwa 实现前台离线访问。
  4. feat(mcp): 新增博客专属 MCP 服务器 + fix(markdown): 修复表格渲染 #17 健康检查探针:暴露 /api/health 供上游工具监控存活状态与配置。
  5. 感觉可以加一个自动化部署 #18 Webhook 服务:解耦异步分发事件给到自定义通知网关。

🛡️ 全局安全与细节审查修复

  • ✅ [Security] 为外链图片下载口加上 AbortController 10s 超时和 10MB 的上限防御(预防 SSRF 与 OOM)
  • ✅ [Stability] 全局修正 parseInt() 引发的非稳健边界异常行为 (防御非法 NaN 入参导致底层 Drizzle 崩溃抛 500 硬错)
  • ✅ [Dependency] 进行深度 npm audit fix,修复依赖项存在的各种 XSS 和提权漏洞 (如 DOMPurify, Vite 等)

请 CodeRabbit Bot 进行 Review 审查!

one-ea and others added 18 commits April 12, 2026 02:34
- 每周一 09:00(北京时间)检查 npm 依赖更新
- 前端/后端分开扫描,自动打 labels
- 忽略主版本升级(避免 breaking changes)
- PR 上限各 5 个,commit 前缀 chore(deps)
- 在 push/PR 时运行 ESLint 并上传 SARIF 到 Code Scanning
- 前端/后端分开扫描
- 结果集成到 GitHub 安全和质量 > 代码扫描页面
- 自动安装 eslint + typescript-eslint + eslint-plugin-security
- 动态生成 eslint.config.mjs(安全规则集)
- 输出 SARIF 上传到 Code Scanning
项目无 package-lock.json,npm ci 会报错
右侧边栏新增 SEO 健康检查:
- 动态指标:Meta 摘要覆盖率 / 标签覆盖率 / URL 规范 / 标题长度
- 固定指标:Sitemap / noindex(404) / JSON-LD / OG 标签
- 百分制评分,绿(90+)/ 黄(70+)/ 红(<70)三色显示
- 基于标签交集相似度推荐,最多展示 3 篇相关文章
- 无标签交集时自动隐藏
- 卡片式布局,支持响应式(移动端单列,桌面端三列)
- 置于评论区之前
- 支持 ```ts{1,3-5} title=app.ts 语法
- 多行代码自动显示左侧行号
- macOS 风格标题栏(红黄绿圆点 + 文件名)
- 指定行高亮(蓝色左边框 + 背景色)
- diff 语言自动着色(+绿 / -红)
- 亮色/暗色模式均适配
- Monaco 编辑器内 Ctrl+V 粘贴图片直接上传到 R2/S3
- 拖拽图片到编辑器自动上传
- 上传过程:先插入占位符 → 上传完成替换为实际URL
- 上传失败自动清理占位符
- 底部快捷键提示新增 Ctrl+V 粘贴图片
- 数据库层:posts 表新增 series_slug + series_order 字段
- 三适配器同步:D1/Turso/PostgreSQL 全部更新
- D1 自动迁移 ensureSeriesColumns()
- API:新增 GET /api/series/:slug 获取同系列文章
- 接口层:Post/PostSummary/CreatePostInput/UpdatePostInput 类型扩展
- 前端组件 series-nav.tsx:系列目录+上/下一篇导航
- 编辑器:元信息面板新增「系列 Slug」输入框
- 样式:系列导航暗色/亮色完整适配
- 新表 reactions(post_slug, type, ip_hash) 三适配器同步
- D1 自动建表 + UNIQUE 约束(post_slug+type+ip_hash 去重)
- API: GET/POST /api/posts/:slug/reactions
- IP 通过 CF-Connecting-IP + SHA-256 哈希匿名化去重
- 前端组件 post-reactions.tsx: 👍❤️🎉🤔 四种 emoji
- 切换逻辑:点击添加/再点取消,服务端 toggle
- localStorage 记忆已点状态,弹跳动画反馈
- 暗色/亮色主题完整适配
- New visits table (SQLite/PG) with path/country/referer/device
- Auto-collect from CF-IPCountry/Referer/User-Agent on post view
- D1/Turso/PG adapters: recordVisit + getAnalytics (5-dim aggregation)
- API: GET /api/admin/analytics?days=N (auth required)
- Frontend: analytics.tsx with trend chart, country, device, referer, top pages
- Dashboard: new Analytics nav entry
- Dark/light theme support
- Markdown renderer: add data-lazy-img + lazy-img class to images
- IntersectionObserver in post.tsx: fade-in on viewport entry
- CSS: smooth opacity+translate animation for progressive loading
- CDN route: Cache-Control immutable (1yr) + Vary: Accept for WebP
- DOMPurify whitelist: data-lazy-img attribute
- Schema: posts.category column (SQLite/PG)
- D1/Turso/PG adapters: category in CRUD + getCategories()
- D1 migration: ensureCategoryColumn()
- API: GET /api/categories (public)
- Editor: category input field
- Home sidebar: category list with counts
- Types: Post/PostSummary/Create/Update all include category
- Add reading mode toggle button in post header
- Add floating ESC/Click exit button
- CSS: hide header, footer, TOC, and reading progress bar
- Increase font size and width for immersive reading
- Use html class .reading-mode for state coupling
@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Summary by CodeRabbit

发布说明

  • 新功能

    • PWA 离线支持与自动更新;阅读模式、分享按钮、文章反应、相关推荐与系列导航
    • 文章版本历史与恢复、分类展示、批量发布/撤回/删除、管理员访客分析仪表板
    • 访客记录与反应统计、公开反应接口与分享/复制交互
  • 改进

    • 路由懒加载与 Suspense 占位、代码块标题/行号/差异高亮、图片渐进式加载与编辑器占位上传
    • 管理后台多选与批量操作、SEO 健康侧栏、分析页面与图表显示
  • 开发工具

    • 添加 Dependabot 配置、ESLint 安全扫描 CI、Vite PWA 集成、定时触发配置

Walkthrough

本 PR 添加文章系列/分类与版本管理、读者反应与分享、访客分析、阅读模式与 PWA 支持;扩展前后端 API、数据库表与存储适配器,实现批量/定时发布、版本快照/恢复与反应切换;引入 ESLint 安全扫描与 Dependabot 配置。

Changes

Cohort / File(s) Summary
工程化与类型声明
/.github/dependabot.yml, /.github/workflows/eslint.yml, server/wrangler.toml, client/src/env.d.ts
新增 Dependabot 配置、ESLint 安全扫描 workflow、Workers 定时触发配置;补充 Vite/PWA 类型引用。
构建/运行时依赖
client/vite.config.ts, client/package.json, server/package.json
在前端加入 VitePWA 插件与配置;client devDependencies 增加 vite-plugin-pwaserver 运行时增加 marked
前端入口与路由
client/src/main.tsx, client/src/app.tsx
注册 PWA service worker(更新提示/离线就绪回调);若干页面改为 React.lazy + Suspense;新增 /admin/analytics 路由。
新 UI 组件与页面
组件
client/src/components/post-reactions.tsx, .../related-posts.tsx, .../series-nav.tsx, .../share-buttons.tsx
页面
client/src/pages/post.tsx, client/src/pages/home.tsx, client/src/pages/admin/{analytics.tsx,dashboard.tsx,editor.tsx}
新增反应、相关推荐、系列导航、分享组件;文章页加入阅读模式、图片渐进加载;首页显示分类;后台新增分析页、版本管理、批量操作、多选等编辑器与 Dashboard 功能变更。
样式与 Markdown 渲染
client/src/globals.css, client/src/lib/markdown.ts
新增代码块标题/行号/行高亮与 diff 样式、懒加载图片类与阅读模式样式;Markdown 渲染支持 code title、行号、行级高亮、diff 行处理,并允许 data-lazy-img 属性。
前端 API 层
client/src/lib/api.ts
扩展类型与 API:系列、分类、反应统计/切换、版本列表/恢复、批量操作、分析数据;updatePost 接受可选 saveVersion 标志。
数据库模式
server/src/db/schema.ts, server/src/db/schema-pg.ts, server/src/migrations/0006_post_versions.sql
在 posts 表增加 series_slug/series_order/category 字段;新增 reactionsvisitspost_versions 表,添加唯一约束与外键。
存储适配器实现
server/src/storage/interfaces.ts, server/src/storage/db/* (d1.ts,postgres.ts,turso.ts)
接口追加版本/批量/定时发布、系列/分类查询、反应与访客分析相关方法;各适配器实现对应表创建、查询与业务逻辑(toggleReaction、recordVisit、getAnalytics、版本快照/恢复、batchOperate、publishScheduled 等)。
服务器路由与业务逻辑
server/src/index.ts
新增公开与 admin API:系列、分类、反应统计/切换、分析、版本查询/恢复、批量操作;记录访问(基于 CF-IPCountry/Referer/UA)、异步通知(Resend/webhook)、边缘缓存中间件、搜索 limit 限制、图片本地化超时/大小保护;将 Worker 导出改为包含 scheduled 处理。
小改动:布局类名
client/src/components/footer.tsx, client/src/components/navbar.tsx
增加 app-footerapp-header CSS 类,仅修改样式类名。

Sequence Diagram(s)

sequenceDiagram
  participant Reader as 读者 (浏览器)
  participant Client as 前端 (SPA)
  participant Server as API (Workers)
  participant DB as 数据库
  Reader->>Client: 点击反应按钮
  Client->>Server: POST /api/posts/:slug/reactions { type }
  Server->>DB: toggleReaction(postSlug, type, ipHash)
  DB-->>Server: { action, reactions }
  Server-->>Client: { action, reactions }
  Client-->>Reader: 更新 UI(计数与状态)
Loading
sequenceDiagram
  participant Scheduler as Workers Scheduled
  participant Server as API (Workers)
  participant DB as 数据库
  participant Webhook as 外部 Webhook
  Scheduler->>Server: scheduled => 执行 publishScheduledPosts()
  Server->>DB: publishScheduledPosts()
  DB-->>Server: 已发布计数
  Server->>Webhook: POST webhook 事件(批量/发布)
  Webhook-->>Server: 200/错误
Loading

估计代码审查工作量

🎯 4 (Complex) | ⏱️ ~50 分钟

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 标题符合 Conventional Commits 格式(feat(v2.0): ...),准确描述了本次更新的核心内容(V2.0 版本完结、批量处理、版本历史与安全修复)。
Description check ✅ Passed PR 描述详细列举了五大新特性、三类安全稳定性修复,并与changeset 中的实际代码变更完全对应(如 Service Worker、版本历史、批量操作、Webhook 等)。

✏️ 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
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code 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.

@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Comment thread client/src/components/post-reactions.tsx Fixed
Comment thread client/src/components/post-reactions.tsx Fixed
Comment thread client/src/lib/markdown.ts Fixed
Comment thread client/src/pages/admin/dashboard.tsx Fixed
@coderabbitai coderabbitai Bot added bug Something isn't working frontend 前端 (React/Vite) 相关变更 backend 后端 (Hono Workers) 相关变更 feature 新功能 labels Apr 12, 2026

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

Actionable comments posted: 20

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (5)
client/src/pages/admin/editor.tsx (2)

555-611: ⚠️ Potential issue | 🟠 Major

系列功能缺了 seriesOrder 的编辑入口。

保存 payload 每次都会带上 seriesOrder,但表单里没有对应输入,新建文章只能一直写入默认 0。如果系列导航按这个字段排序,结果会变成不稳定或全部并列。建议把排序字段也放进元信息面板,或者在后端为同系列自动分配顺序。

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

In `@client/src/pages/admin/editor.tsx` around lines 555 - 611, The form is
missing an input for seriesOrder so new posts always submit the default 0; add a
small numeric input bound to form.seriesOrder and wired to the same updateField
handler (updateField("seriesOrder", ...)) inside the metadata grid next to
seriesSlug (or appropriate spot), ensure its value uses form.seriesOrder and it
parses/validates numeric input (e.g., parseInt or Number before updating) and
keep any disabled/isEdit behavior consistent with other fields so the payload
includes the intended order when saving.

453-458: ⚠️ Potential issue | 🟠 Major

Monaco 编辑器的 drop 事件缺少冒泡阻止,导致拖拽图片时重复上传。

外层容器的 onDrop 处理程序和 Monaco DOM 节点的 drop 监听器都会被触发。Monaco 的 paste 事件正确调用了 stopPropagation(),但 drop 事件(665-693 行)缺少此调用,图片拖入编辑器时会同时触发两个 handleImageUpload() 调用,产生重复占位符和重复网络请求。

建议在 Monaco drop 事件处理中补充 de.stopPropagation();同时应在编辑器卸载时清理这些 DOM 事件监听器,避免内存泄漏。

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

In `@client/src/pages/admin/editor.tsx` around lines 453 - 458, The Monaco
editor's drop handler is missing de.stopPropagation(), causing both the outer
div's onDrop (handleDrop) and Monaco's internal drop listener to fire and
double-upload images; update the Monaco drop event listener to call
event.stopPropagation() (and optionally event.preventDefault()) inside the
handler that currently calls handleImageUpload, and ensure those DOM listeners
are removed when the editor unmounts/disposes by calling
removeEventListener/dispose in the editor cleanup path (the code that attaches
Monaco's 'drop' listener where handleImageUpload is invoked and the component
unmount/dispose logic that currently registers editor listeners).
client/src/pages/admin/dashboard.tsx (1)

31-38: ⚠️ Potential issue | 🟠 Major

鉴权失败分支会把页面卡在加载态。

checkAuth() 这里只有成功回调;一旦网络异常或接口 5xx,既不会跳转,也不会 setLoading(false),后台会一直停在 skeleton。建议给整条链路补 catch/finally,并显示错误提示。

As per coding guidelines "数据加载和错误处理是否完善".

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

In `@client/src/pages/admin/dashboard.tsx` around lines 31 - 38, The auth branch
inside the useEffect currently only handles the successful checkAuth() path so
network or 5xx errors leave the page stuck loading; update the checkAuth()
promise chain to add catch and finally handlers that (a) on error call
setLoading(false) and surface an error (e.g., set an error state or show a
toast), (b) ensure non-authorized responses still call
setLocation("/admin/login") and setLoading(false), and (c) ensure subsequent
fetchAdminPosts() and fetchViewStats() also have catch/finally so they cannot
leave loading true; reference useEffect, checkAuth, setLocation, setLoading,
fetchAdminPosts, and fetchViewStats when making the changes.
server/src/index.ts (1)

643-670: ⚠️ Potential issue | 🔴 Critical

批量图片本地化把安全补丁绕回去了。

单篇路径只检查了 content-length 头,而批量路径仍然是无超时、无 10MB 实际字节上限、无 content-type 校验地 arrayBuffer() 整体读入。只要远端不返回 content-length,或者直接回超大/非图片响应,这个 admin 接口仍然能把 Worker 内存打满,并把任意内容写到 /cdn/*。建议把下载逻辑抽成共享的安全 helper:统一限制 scheme、超时、实际读取字节数,并强制 content-typeimage/ 开头后再写入存储。

Also applies to: 708-729

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

In `@server/src/index.ts` around lines 643 - 670, The batch-download loop
currently calls resp.arrayBuffer() and writes to storage without robust
checks—create a shared helper (e.g., fetchAndStreamImage or
downloadImageWithLimits) and use it both in the externalUrls loop and the other
path referenced (lines ~708-729): the helper must validate URL scheme
(http/https), apply an AbortController timeout, check resp.ok, require
Content-Type to start with "image/", and then read resp.body via a reader while
enforcing a hard byte cap (10 * 1024 * 1024) so you stop and abort if exceeded;
return a safe ReadableStream or buffer and the contentType/extension (used by
storage.put and key generation) so callers no longer call resp.arrayBuffer()
directly or trust content-length header.
server/src/storage/db/d1.ts (1)

29-41: ⚠️ Potential issue | 🔴 Critical

D1 初始化漏掉了 postVersions

ensureSchema() 已经补了 reactions / visits / series / category,但版本历史依赖的 postVersions 没有在这里初始化。下面的 getPostVersions() / createPostVersion() / restorePostVersion() 在首次使用时会直接打到不存在的表。建议补一个 ensurePostVersionsTable() 并放进同一条初始化链路。 As per coding guidelines, server/src/storage/** 审查需关注:1. 接口实现是否完整;4. 错误处理和边界情况。

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

In `@server/src/storage/db/d1.ts` around lines 29 - 41, The D1 schema
initialization misses the postVersions table; add a new helper
ensurePostVersionsTable() and include it in the existing initialization chain
inside ensureSchema() (the same async IIFE that calls ensureViewCountColumn,
ensurePinnedColumn, ensureSettingsTable, ensurePagesTable, ensureCommentsTable,
ensureSeriesColumns, ensureReactionsTable, ensureVisitsTable,
ensureCategoryColumn) so the table exists before any calls to getPostVersions(),
createPostVersion(), or restorePostVersion(); implement
ensurePostVersionsTable() to create the postVersions table if not present and
surface/propagate errors consistently with the other ensure* helpers.
🟡 Minor comments (5)
server/src/db/schema.ts-23-25 (1)

23-25: ⚠️ Potential issue | 🟡 Minor

category 建议设为 NOT NULL 以保持类型一致

category 目前可空,容易把 null 传到前端字符串分支。若业务上“无分类”就是空字符串,建议直接 notNull().default("")

建议修复(示例)
-  category: text("category").default(""),
+  category: text("category").notNull().default(""),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/db/schema.ts` around lines 23 - 25, The schema's category column
is nullable but should be non-null to avoid null strings reaching the frontend;
update the column definition from text("category").default("") to
text("category").notNull().default("") in the schema (the symbol to change is
category in the table definition using text(), notNull(), default()) so the DB
enforces an empty-string default instead of NULL.
client/src/pages/home.tsx-86-86 (1)

86-86: ⚠️ Potential issue | 🟡 Minor

分类请求错误被静默吞掉,建议至少做可观测处理

catch(() => {}) 会让分类接口失败不可见,排障成本高。建议至少 console.warn,更好是落到页面级错误态。

建议修复(最小改动)
-    fetchCategories().then(setCategories).catch(() => {});
+    fetchCategories()
+      .then(setCategories)
+      .catch((err) => {
+        console.warn("分类加载失败", err);
+      });

As per coding guidelines, client/src/pages/**: 页面级组件。审查时请关注: 1. 数据加载和错误处理是否完善 2. SEO 相关(页面标题、meta 标签) 3. 导航和路由是否正确。

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

In `@client/src/pages/home.tsx` at line 86, The current call
fetchCategories().then(setCategories).catch(() => {}) silently swallows errors;
change the catch to surface the failure by at minimum logging the error (e.g.
.catch(err => console.warn('fetchCategories failed', err))) and ideally set a
page-level error state (add a useState like [categoriesError,
setCategoriesError] and in the catch call setCategoriesError(err)) so the
component can render an error message; update relevant render logic to show a
user-facing error when categoriesError is set.
client/src/pages/home.tsx-199-203 (1)

199-203: ⚠️ Potential issue | 🟡 Minor

分类项样式暗示“可点击”,但当前没有任何导航或交互

这里用了 cursor-pointer/hover 态,但节点是纯 div 且无路由跳转,用户预期会落空。建议改为真实链接(可键盘访问),或去掉点击态样式。

建议修复(若已有分类页路由)
-                      <div key={cat.name} className="flex items-center justify-between py-[3px] px-[6px] rounded-md hover:bg-accent/20 transition-colors cursor-pointer group">
+                      <a
+                        key={cat.name}
+                        href={`/categories/${encodeURIComponent(cat.name)}`}
+                        className="flex items-center justify-between py-[3px] px-[6px] rounded-md hover:bg-accent/20 transition-colors group"
+                      >
                         <span className="text-[12px] text-muted-foreground group-hover:text-foreground transition-colors">{cat.name}</span>
                         <span className="text-[10px] font-mono text-muted-foreground/30">{cat.count}</span>
-                      </div>
+                      </a>

As per coding guidelines, client/src/pages/**: 页面级组件。审查时请关注: 1. 数据加载和错误处理是否完善 2. SEO 相关(页面标题、meta 标签) 3. 导航和路由是否正确。

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

In `@client/src/pages/home.tsx` around lines 199 - 203, The category item JSX
using the div with key={cat.name} and className including "cursor-pointer" and
hover styles is misleading because it looks clickable but has no navigation or
keyboard access; either convert that div into a real interactive element (e.g.,
replace the div with a Link/a/button component that navigates to the category
route and uses href or onClick, preserves the span children showing cat.name and
cat.count, and adds focus/aria attributes for keyboard accessibility) or remove
the "cursor-pointer" and hover classes so it no longer implies clickability;
update the element that renders key={cat.name} accordingly and ensure routing
uses the app's router/link component and the displayed cat.name and cat.count
remain intact.
client/src/components/series-nav.tsx-21-23 (1)

21-23: ⚠️ Potential issue | 🟡 Minor

处理 currentSlug 未匹配的边界情况

currentSlug 在系列中未找到,currentIndex-1,进度显示为 "0 / N",prev/next 逻辑也会异常。

建议添加防御性检查:

🛡️ 添加边界检查
   const currentIndex = posts.findIndex((p) => p.slug === currentSlug);
+  // 当前文章不在系列中,不渲染导航
+  if (currentIndex === -1) return null;
+
   const prev = currentIndex > 0 ? posts[currentIndex - 1] : null;
   const next = currentIndex < posts.length - 1 ? posts[currentIndex + 1] : null;

Also applies to: 37-39

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

In `@client/src/components/series-nav.tsx` around lines 21 - 23, The code assumes
currentSlug exists in posts; when posts.findIndex returns -1 the prev/next logic
and progress display break. Add a defensive check after computing currentIndex:
if currentIndex === -1, set prev and next to null (and use a special display
index or placeholder for progress rather than 0), otherwise compute prev/next as
currently done; update the same guard around the second occurrence of this logic
(the block referencing currentIndex at lines 37-39) so both prev/next and the
progress UI handle a missing currentSlug safely. Use the existing symbols
currentIndex, prev, next, posts, and currentSlug to locate and modify the logic.
client/src/pages/post.tsx-244-250 (1)

244-250: ⚠️ Potential issue | 🟡 Minor

给浮动退出按钮补上可访问名称。

这是纯图标按钮,当前只有 title,读屏器拿不到稳定的可访问名称。这里最好补一个 aria-label="退出阅读模式",避免阅读模式入口对辅助技术用户不可见。

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

In `@client/src/pages/post.tsx` around lines 244 - 250, The floating exit button
uses only an icon and a title so screen readers lack a stable accessible name;
update the button element (the one with onClick={toggleReadingMode} and
className="reading-mode-exit-fab") to include an explicit accessible name such
as aria-label="退出阅读模式" (or aria-labelledby if you prefer a referenced label) so
assistive technology can announce the control while keeping the existing title
and icon (<X />) intact.
🧹 Nitpick comments (8)
server/wrangler.toml (1)

22-24: 建议按环境控制 Cron 频率,避免低流量时空转成本

每分钟触发在功能上没问题,但建议至少在非生产环境关闭或降频(如 */5 * * * *),减少 D1 查询与日志噪音。

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

In `@server/wrangler.toml` around lines 22 - 24, The scheduled cron under
[triggers] currently runs every minute (crons = ["* * * * *"]); update it to be
environment-aware so non-production avoids the 1-minute cadence—for example, set
crons to a lower frequency like "*/5 * * * *" or disable the trigger when ENV ≠
production; implement the environment check where wrangler.toml is generated or
read (the [triggers] section / crons key) so production keeps the current
schedule while staging/dev use a reduced or disabled schedule.
client/src/pages/admin/analytics.tsx (1)

33-39: 建议显示错误状态而非静默忽略

当前 catch(() => {}) 静默吞掉错误,用户无法区分"无数据"与"加载失败"。

♻️ 添加错误状态处理
+  const [error, setError] = useState(false);

   useEffect(() => {
     setLoading(true);
+    setError(false);
     fetchAnalytics(days)
       .then(setData)
-      .catch(() => {})
+      .catch(() => setError(true))
       .finally(() => setLoading(false));
   }, [days]);

// 在渲染逻辑中添加错误状态
   {loading ? (
     <div className="...">加载中...</div>
+  ) : error ? (
+    <div className="text-center text-red-400/60 py-[60px]">加载失败,请稍后重试</div>
   ) : !data ? (

根据 coding guidelines:「数据加载和错误处理是否完善」。

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

In `@client/src/pages/admin/analytics.tsx` around lines 33 - 39, The useEffect
swallowing errors via catch(() => {}) hides failures; replace the empty catch by
capturing the error from fetchAnalytics and setting an error state (e.g., call
setError(error) or setFetchError with a user-friendly message) and optionally
clear or set data to an empty value via setData when an error occurs; ensure
setLoading(false) remains in finally. If an error state hook doesn't exist, add
a useState for error and surface it in the UI so callers of useEffect,
fetchAnalytics, setData and setLoading can show "load failed" vs "no data".
client/src/lib/markdown.ts (1)

85-89: 建议处理逆序范围

若用户输入 {5-1}(逆序范围),当前逻辑 for (let i = start; i <= end; i++) 不会添加任何行。建议交换边界或跳过无效范围:

♻️ 防御性范围处理
         if (rangeMatch) {
-          const start = parseInt(rangeMatch[1]);
-          const end = parseInt(rangeMatch[2]);
+          let start = parseInt(rangeMatch[1]);
+          let end = parseInt(rangeMatch[2]);
+          if (start > end) [start, end] = [end, start];
           for (let i = start; i <= end; i++) highlightLines.add(i);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/lib/markdown.ts` around lines 85 - 89, The range parser in
client/src/lib/markdown.ts currently assumes start <= end when handling the
/^(\d+)-(\d+)$/ match inside the loop that builds highlightLines; if a user
supplies a reverse range like "{5-1}" nothing is added. Update the logic in the
block that processes rangeMatch (the code that reads start/end and iterates to
add to highlightLines) to defensively handle reverse ranges by either swapping
start and end before the for-loop or explicitly skipping/normalizing invalid
ranges so all intended lines get added; keep the rangeMatch parsing and
highlightLines.add usage but ensure start and end are normalized before
iterating.
client/src/components/related-posts.tsx (1)

19-21: 考虑优化:获取全部文章可能影响性能

当前实现获取所有文章后在客户端过滤。文章数量增多后(如 100+ 篇),会增加不必要的网络传输和内存占用。

建议后续迭代中增加服务端 /api/posts/:slug/related 端点,直接返回相关文章。当前方案在文章较少时可接受。

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

In `@client/src/components/related-posts.tsx` around lines 19 - 21, The client
currently fetches all posts in useEffect
(fetchPosts().then(setAllPosts).catch(() => {})) and then filters client-side,
which will scale poorly; change the component to call a server endpoint that
returns only related posts (e.g., GET /api/posts/:slug/related) instead of
fetchPosts, passing the current post slug, and set the response into setAllPosts
(or rename to setRelatedPosts) and handle errors/logging (replace the empty
catch). Update references in related-posts.tsx to use useEffect and the new
fetch path and ensure the component expects the reduced payload shape.
client/src/components/series-nav.tsx (1)

29-40: 建议增强按钮无障碍属性

折叠按钮缺少 aria-expanded 属性,屏幕阅读器用户无法感知展开状态。

♻️ 添加 aria-expanded
         <button
           onClick={() => setExpanded(!expanded)}
           className="series-nav__toggle"
+          aria-expanded={expanded}
+          aria-controls="series-nav-list"
         >

根据 coding guidelines:「无障碍访问(aria 标签、键盘导航)」。

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

In `@client/src/components/series-nav.tsx` around lines 29 - 40, The toggle button
in series-nav.tsx is missing an aria-expanded attribute so screen readers can't
detect its state; update the <button> that calls setExpanded(!expanded) to
include aria-expanded tied to the expanded state (aria-expanded={expanded}), and
if the collapsible content has an ID, also add aria-controls referencing that ID
to associate the button with the controlled region; locate the button using
symbols setExpanded and expanded in the series-nav component and add these ARIA
attributes accordingly.
server/src/migrations/0006_post_versions.sql (1)

1-9: 建议为 post_id 添加索引

按文章查询版本历史时会频繁使用 WHERE post_id = ?,建议添加索引以提升查询性能:

♻️ 添加索引
 CREATE TABLE `post_versions` (
 	`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
 	`post_id` integer NOT NULL,
 	`title` text NOT NULL,
 	`content` text NOT NULL,
 	`excerpt` text DEFAULT '',
 	`created_at` text DEFAULT (datetime('now')) NOT NULL,
 	FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`) ON UPDATE no action ON DELETE cascade
 );
+
+CREATE INDEX `idx_post_versions_post_id` ON `post_versions`(`post_id`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/migrations/0006_post_versions.sql` around lines 1 - 9, The
migration creates the post_versions table but lacks an index on post_id, causing
slow WHERE post_id = ? queries; update the migration to add an index on the
post_id column (e.g., create an index named like idx_post_versions_post_id on
post_versions(post_id)) so lookups by post_id are fast, and if your migration
system supports up/down or rollback, also add the corresponding DROP INDEX for
cleanup; target symbols: table post_versions and column post_id.
client/src/app.tsx (1)

77-85: 建议添加错误边界处理懒加载失败

Suspense 的 fallback 仅处理加载中状态。若动态 import() 失败(网络问题、chunk 加载错误),会导致未捕获异常。

建议在 Suspense 外层包裹 ErrorBoundary,优雅处理 chunk 加载失败场景:

♻️ 建议添加 ErrorBoundary
// 可使用 react-error-boundary 或自定义 ErrorBoundary
import { ErrorBoundary } from 'react-error-boundary';

function ChunkLoadError({ resetErrorBoundary }: { resetErrorBoundary: () => void }) {
  return (
    <div className="p-8 text-center">
      <p className="text-muted-foreground mb-4">页面加载失败</p>
      <button onClick={resetErrorBoundary} className="text-cyan-400 hover:underline">
        重试
      </button>
    </div>
  );
}

// 在 Suspense 外层使用
<ErrorBoundary FallbackComponent={ChunkLoadError}>
  <Suspense fallback={...}>
    <Switch>...</Switch>
  </Suspense>
</ErrorBoundary>

Also applies to: 89-124

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

In `@client/src/app.tsx` around lines 77 - 85, Wrap the Suspense that renders
<Switch>/<Route>/<ProtectedRoute>/<AdminEditor> with an ErrorBoundary (e.g.,
import { ErrorBoundary } from 'react-error-boundary' or a custom class) so
chunk-load failures are caught; add a small FallbackComponent (e.g.,
ChunkLoadError) that accepts resetErrorBoundary and shows a retry button, pass
FallbackComponent to ErrorBoundary, and place Suspense inside it (instead of
vice versa); repeat the same wrapping for the other Suspense usages in this file
that lazy-load routes/components.
server/src/db/schema-pg.ts (1)

93-100: 访问日志表最好现在就补时间索引。

visits 是持续增长的表,分析接口会长期按 created_at 做时间窗过滤。没有索引的话,数据量一上来就会退化成全表扫描。建议至少给 created_at 建索引,再按实际查询量决定是否补联合索引。

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

In `@server/src/db/schema-pg.ts` around lines 93 - 100, The visits table
(pgVisits) lacks an index on the time column (createdAt), causing scans for
time-window queries; add a dedicated index on the created_at column (e.g.,
idx_visits_created_at) — either by adding an index declaration alongside the
pgTable definition if your schema API supports it or by adding a database
migration that creates an index on visits.created_at (and deploy the migration);
ensure you reference the pgVisits/createdAt symbol and run the migration so
future queries filter by created_at use the new index.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/src/components/post-reactions.tsx`:
- Around line 62-66: The reaction button is only visual (emoji) and lacks
accessible semantics and state; update the button rendered in the post-reactions
component (the element using handleReaction, activeTypes, and animating) to
include aria-label={label} and aria-pressed={activeTypes.has(type)} so screen
readers get a stable name and the current toggle state; ensure these attributes
are set on the same <button> that already uses onClick and the className logic.
- Around line 29-50: handleReaction allows concurrent requests for the same
post/type which can reorder and overwrite counts/activeTypes; add a per-type
"inFlight" lock (e.g., a Set or Map in component state or ref) and early-return
if the type is already in-flight, then add the type to the lock before calling
toggleReaction(slug, type) and remove it in finally; update setCounts and
setActiveTypes only after confirming the request completed and persist
localStorage as before; also reflect the lock in the UI by disabling the
reaction button when inFlight.has(type) (or by using the same inFlight ref to
drive the disabled prop) to prevent further clicks while the request is
outstanding.

In `@client/src/components/share-buttons.tsx`:
- Around line 49-79: The icon-only share controls (the Twitter <a>, LinkedIn
<a>, copy <button> using handleCopy and copied, and native share <button> with
handleNativeShare) lack proper aria labels; add aria-label attributes to each
control matching their visible title (e.g., "分享到 X", "分享到 LinkedIn", "复制链接",
"系统分享") and for the copy button update the aria-label dynamically when copied is
true (e.g., switch to "已复制链接!") so the accessible name stays in sync with the
title/state; ensure these aria-label strings are used instead of relying solely
on title for screen readers and keyboard users.

In `@client/src/globals.css`:
- Around line 1520-1529: The light-theme selector :root:not(.dark) used on the
.reading-mode-exit-fab rules incorrectly matches the default dark root; change
both selector occurrences back to the project's canonical light selector
[data-theme="light"] .reading-mode-exit-fab so the light styles apply only when
the light theme is active and do not override dark-theme variables.
- Around line 1455-1462: The current rule uses generic selectors ".reading-mode
header" and ".reading-mode footer" which hide article-level <header>/<footer>;
remove those generic selectors and instead target only site-shell elements (e.g.
replace them with specific selectors like ".reading-mode .site-header",
".reading-mode .site-footer", ".reading-mode .app-header", ".reading-mode
.app-footer", ".reading-mode .site-nav") while keeping ".reading-mode
.reading-progress-bar", ".reading-mode .toc-desktop", ".reading-mode
.toc-mobile", ".reading-mode .reading-mode-toggle"; ensure you do not include
"article > header", ".post-header", ".entry-header" or other article-specific
header/footer selectors so the article title and meta remain visible in reading
mode.

In `@client/src/pages/admin/dashboard.tsx`:
- Around line 55-87: selectedSlugs is not kept in sync with filteredPosts so
batch actions in handleBatchOperate can affect hidden posts; fix by restricting
the slugs acted on to the intersection with current filteredPosts before calling
batchOperatePosts (e.g., compute const visibleSlugs = slugs.filter(s =>
filteredPosts.some(p => p.slug === s)) and use visibleSlugs for the API call and
local state updates), and optionally clear or trim selectedSlugs whenever
filteredPosts changes (use setSelectedSlugs(new Set([...selectedSlugs].filter(s
=> filteredPosts.some(p => p.slug === s)))) in the component effect) to avoid
acting on out-of-filter selections; update references in handleBatchOperate,
toggleSelectAll, and any effect that watches filteredPosts.

In `@client/vite.config.ts`:
- Around line 36-44: The runtime caching rule currently uses urlPattern:
/^https:\/\/images\.monolith\.example\/.*$/i which won't match your actual image
requests served from the /cdn/ path; update the urlPattern in that caching entry
(the object with cacheName: "monolith-images") to match the /cdn/ route instead
(for example use a RegExp like /^\/cdn\/.*$/i or /\/cdn\//) so the CacheFirst
handler actually hits image requests.
- Around line 20-31: The PWA config references missing asset files
(includeAssets listing favicon.ico, apple-touch-icon.png, mask-icon.svg and
manifest.icons entries icon-192.png, icon-512.png) causing manifest validation
and runtime issues; fix by adding the referenced files into the public assets
(place valid favicon.ico, apple-touch-icon.png, mask-icon.svg, icon-192.png and
icon-512.png) or remove their references from the includeAssets array and the
manifest.icons block in vite PWA config (look for includeAssets and the icons
array in the manifest to update), ensuring filenames, sizes and MIME types match
the manifest entries before production.

In `@server/src/db/schema-pg.ts`:
- Around line 84-90: The pgReactions table lacks a uniqueness constraint and a
foreign key, so concurrent clicks can create duplicate rows and reactions become
orphaned when posts are deleted; update the schema by replacing or supplementing
postSlug with a postId column (e.g., postId: integer("post_id").notNull()) that
references the posts table primary key (add an ON DELETE CASCADE if desired),
and add a unique constraint/index on (post_id, type, ip_hash) to enforce one
reaction per IP/type/post; update the pgReactions definition (and any code using
postSlug) and add a DB migration to create post_id as a FK and the unique
constraint to prevent duplicates.

In `@server/src/db/schema.ts`:
- Around line 83-91: reactions 表目前只用 id 主键且用 postSlug 字段无法保证引用完整性,且没有对 (post,
type, ip) 做唯一约束,导致重复计数和孤儿数据风险;修改方案:在 reactions 表定义中添加一个外键字段(例如 postId integer)并
reference 到 posts 表的主键(posts.id 或项目中实际使用的 posts 主键),同时在 reactions 上增加唯一约束
UNIQUE(postId, type, ipHash)(或如果必须保留 slug,则改为 UNIQUE(postSlug, type, ipHash)
并同时添加 FOREIGN KEY(postSlug) REFERENCES
posts(slug)),并记得为该变更创建相应数据库迁移以清理/迁移现有数据并维护约束。

In `@server/src/index.ts`:
- Around line 564-574: The route handler for POST
"/api/admin/posts/:slug/versions/:id/restore" currently calls
db.restorePostVersion(slug, versionId) before creating a snapshot, so the
subsequent db.createPostVersion(slug) records the restored content and destroys
the ability to undo; switch the order so you call db.createPostVersion(slug)
first (to snapshot the current content), then call db.restorePostVersion(slug,
versionId), and return the restored post; update variable usage around
versionId/post accordingly and keep error checks the same.
- Around line 578-586: The route handler for app.post("/api/admin/posts/batch")
validates slugs but not the action; add a runtime check that the parsed action
from c.req.json is one of the allowed values ("publish", "unpublish", "delete")
before calling db.batchOperatePosts. If action is missing or invalid, return a
400 JSON error (similar style to the slugs check). Update the handler around the
variables { slugs, action } and ensure you don't call db.batchOperatePosts or
triggerWebhook when action fails validation.

In `@server/src/storage/db/d1.ts`:
- Around line 887-901: Both getSeriesPosts and getCategories only check
posts.published = 1 and therefore include scheduled posts; update their WHERE
filters to match the published-visibility rule used by getPublishedPosts() by
requiring posts.published = 1 AND (posts.publish_at IS NULL OR posts.publish_at
<= datetime('now')) (use the same sql template and identifier symbols:
getSeriesPosts, getCategories, posts.publish_at, posts.published) so scheduled
posts are excluded; ensure the SQL/knex-style condition is composed with the
existing sql`` helper in getSeriesPosts and the raw SQL string in getCategories
and keep result mapping unchanged.
- Around line 965-983: getAnalytics currently builds a raw SQL fragment from the
days argument (via const since = `datetime('now','-${days} days')` and
sql.raw(since)), creating SQL injection risk; update getAnalytics to
validate/normalize days to a non-negative integer inside the function (e.g.,
clamp/parseInt and throw or set default if invalid) and stop using sql.raw —
instead pass days as a bound parameter to db.run calls (remove sql.raw(since)
and use a parameterized expression based on days), keeping the queries in
getAnalytics/ensureVisitsTable/db.run and the same GROUP/BY logic but using
parameter binding for days so D1/Turso implementations mirror the safe
PostgreSQL approach.

In `@server/src/storage/db/postgres.ts`:
- Around line 902-907: getReactions() can fail on a fresh Postgres instance
because the reactions table is created lazily in toggleReaction(); ensure the
table exists before any reads by either moving reactions table creation into the
unified storage initialization or by performing an existence/CREATE TABLE IF NOT
EXISTS check inside the read path; update getReactions() (and/or server storage
init) to call the table-ensure routine (referencing getReactions(),
toggleReaction(), and the reactions table) so the SELECT will never run against
a missing table and handle any errors gracefully.
- Around line 884-899: The public queries getSeriesPosts and getCategories
currently only filter on published = true and thus include future-scheduled
posts; update their WHERE clauses to match getPublishedPosts()/searchPosts() by
adding the same time visibility condition (publish_at <= NOW() or the project's
canonical publish timestamp column) so future posts are excluded, ensure you use
the same column name/casing as other methods, keep the existing field
casts/mapping (slug/title/series_order and category/count) and preserve behavior
consistent with other backends/error handling.
- Around line 368-389: The code calls pgPostVersions in getPostVersions and
createPostVersion but ensureCoreTables still only creates the old core tables
and never adds the pg_post_versions table or the new columns series_slug,
series_order, category, so fresh PG installs or upgraded DBs will hit
“relation/column does not exist”; update the initialization/migration logic
(ensureCoreTables or a new idempotent migration helper invoked before any
reads/writes) to CREATE TABLE IF NOT EXISTS pg_post_versions (matching the
PostVersion shape) and ALTER TABLE ... ADD COLUMN IF NOT EXISTS for series_slug,
series_order, category, and ensure createPostVersion/getPostVersions either call
that initializer or perform a safe existence check for pgPostVersions and
surface a clear error; keep changes idempotent and consistent with other
backends and add appropriate error handling around DB operations.

In `@server/src/storage/db/turso.ts`:
- Around line 254-256: The Turso adapter is missing schema migrations for new
columns/tables (series_slug, category, series_order and
postVersions/reactions/visits); update the Turso implementation by adding a
centralized migration method (mirror D1Adapter.ensureSchema()) and extend
ensureCoreTables() / TursoAdapter.ensureSchema() to create the missing columns
and tables and run it at startup; ensure the column names referenced
(seriesSlug/series_slug, category, seriesOrder/series_order) match usage, add
idempotent CREATE TABLE / ALTER TABLE statements, propagate errors via the
adapter's error handling, and keep behavior consistent with D1Adapter and other
backends.
- Around line 875-889: The visibility filter for scheduled posts is missing in
getSeriesPosts and getCategories; update the WHERE clauses in getSeriesPosts
(the select using posts.seriesSlug / posts.published) and the raw SQL in
getCategories so they require the same publish-time check used by
getPublishedPosts — add the condition (publish_at IS NULL OR publish_at <=
datetime('now')) alongside published = 1 (and keep the existing category !=
''/IS NOT NULL check) so D1/Turso/Postgres behavior matches existing
getPublishedPosts visibility rules.
- Around line 940-951: The getAnalytics method currently builds the since
expression with string interpolation and uses sql.raw(), which bypasses
parameterization; validate that the days parameter is a non-negative integer
(e.g., parseInt and throw or clamp if invalid) and then embed it into a Drizzle
SQL template instead of sql.raw (replace the since/sql.raw usage with a
parameterized expression using sql`datetime('now', '-${days} days')` or
equivalent), and update all five queries (byDay, byCountry, byReferer, byDevice,
byPage) to reference that parameterized since expression so inputs are safely
escaped and injection is prevented.

---

Outside diff comments:
In `@client/src/pages/admin/dashboard.tsx`:
- Around line 31-38: The auth branch inside the useEffect currently only handles
the successful checkAuth() path so network or 5xx errors leave the page stuck
loading; update the checkAuth() promise chain to add catch and finally handlers
that (a) on error call setLoading(false) and surface an error (e.g., set an
error state or show a toast), (b) ensure non-authorized responses still call
setLocation("/admin/login") and setLoading(false), and (c) ensure subsequent
fetchAdminPosts() and fetchViewStats() also have catch/finally so they cannot
leave loading true; reference useEffect, checkAuth, setLocation, setLoading,
fetchAdminPosts, and fetchViewStats when making the changes.

In `@client/src/pages/admin/editor.tsx`:
- Around line 555-611: The form is missing an input for seriesOrder so new posts
always submit the default 0; add a small numeric input bound to form.seriesOrder
and wired to the same updateField handler (updateField("seriesOrder", ...))
inside the metadata grid next to seriesSlug (or appropriate spot), ensure its
value uses form.seriesOrder and it parses/validates numeric input (e.g.,
parseInt or Number before updating) and keep any disabled/isEdit behavior
consistent with other fields so the payload includes the intended order when
saving.
- Around line 453-458: The Monaco editor's drop handler is missing
de.stopPropagation(), causing both the outer div's onDrop (handleDrop) and
Monaco's internal drop listener to fire and double-upload images; update the
Monaco drop event listener to call event.stopPropagation() (and optionally
event.preventDefault()) inside the handler that currently calls
handleImageUpload, and ensure those DOM listeners are removed when the editor
unmounts/disposes by calling removeEventListener/dispose in the editor cleanup
path (the code that attaches Monaco's 'drop' listener where handleImageUpload is
invoked and the component unmount/dispose logic that currently registers editor
listeners).

In `@server/src/index.ts`:
- Around line 643-670: The batch-download loop currently calls
resp.arrayBuffer() and writes to storage without robust checks—create a shared
helper (e.g., fetchAndStreamImage or downloadImageWithLimits) and use it both in
the externalUrls loop and the other path referenced (lines ~708-729): the helper
must validate URL scheme (http/https), apply an AbortController timeout, check
resp.ok, require Content-Type to start with "image/", and then read resp.body
via a reader while enforcing a hard byte cap (10 * 1024 * 1024) so you stop and
abort if exceeded; return a safe ReadableStream or buffer and the
contentType/extension (used by storage.put and key generation) so callers no
longer call resp.arrayBuffer() directly or trust content-length header.

In `@server/src/storage/db/d1.ts`:
- Around line 29-41: The D1 schema initialization misses the postVersions table;
add a new helper ensurePostVersionsTable() and include it in the existing
initialization chain inside ensureSchema() (the same async IIFE that calls
ensureViewCountColumn, ensurePinnedColumn, ensureSettingsTable,
ensurePagesTable, ensureCommentsTable, ensureSeriesColumns,
ensureReactionsTable, ensureVisitsTable, ensureCategoryColumn) so the table
exists before any calls to getPostVersions(), createPostVersion(), or
restorePostVersion(); implement ensurePostVersionsTable() to create the
postVersions table if not present and surface/propagate errors consistently with
the other ensure* helpers.

---

Minor comments:
In `@client/src/components/series-nav.tsx`:
- Around line 21-23: The code assumes currentSlug exists in posts; when
posts.findIndex returns -1 the prev/next logic and progress display break. Add a
defensive check after computing currentIndex: if currentIndex === -1, set prev
and next to null (and use a special display index or placeholder for progress
rather than 0), otherwise compute prev/next as currently done; update the same
guard around the second occurrence of this logic (the block referencing
currentIndex at lines 37-39) so both prev/next and the progress UI handle a
missing currentSlug safely. Use the existing symbols currentIndex, prev, next,
posts, and currentSlug to locate and modify the logic.

In `@client/src/pages/home.tsx`:
- Line 86: The current call fetchCategories().then(setCategories).catch(() =>
{}) silently swallows errors; change the catch to surface the failure by at
minimum logging the error (e.g. .catch(err => console.warn('fetchCategories
failed', err))) and ideally set a page-level error state (add a useState like
[categoriesError, setCategoriesError] and in the catch call
setCategoriesError(err)) so the component can render an error message; update
relevant render logic to show a user-facing error when categoriesError is set.
- Around line 199-203: The category item JSX using the div with key={cat.name}
and className including "cursor-pointer" and hover styles is misleading because
it looks clickable but has no navigation or keyboard access; either convert that
div into a real interactive element (e.g., replace the div with a Link/a/button
component that navigates to the category route and uses href or onClick,
preserves the span children showing cat.name and cat.count, and adds focus/aria
attributes for keyboard accessibility) or remove the "cursor-pointer" and hover
classes so it no longer implies clickability; update the element that renders
key={cat.name} accordingly and ensure routing uses the app's router/link
component and the displayed cat.name and cat.count remain intact.

In `@client/src/pages/post.tsx`:
- Around line 244-250: The floating exit button uses only an icon and a title so
screen readers lack a stable accessible name; update the button element (the one
with onClick={toggleReadingMode} and className="reading-mode-exit-fab") to
include an explicit accessible name such as aria-label="退出阅读模式" (or
aria-labelledby if you prefer a referenced label) so assistive technology can
announce the control while keeping the existing title and icon (<X />) intact.

In `@server/src/db/schema.ts`:
- Around line 23-25: The schema's category column is nullable but should be
non-null to avoid null strings reaching the frontend; update the column
definition from text("category").default("") to
text("category").notNull().default("") in the schema (the symbol to change is
category in the table definition using text(), notNull(), default()) so the DB
enforces an empty-string default instead of NULL.

---

Nitpick comments:
In `@client/src/app.tsx`:
- Around line 77-85: Wrap the Suspense that renders
<Switch>/<Route>/<ProtectedRoute>/<AdminEditor> with an ErrorBoundary (e.g.,
import { ErrorBoundary } from 'react-error-boundary' or a custom class) so
chunk-load failures are caught; add a small FallbackComponent (e.g.,
ChunkLoadError) that accepts resetErrorBoundary and shows a retry button, pass
FallbackComponent to ErrorBoundary, and place Suspense inside it (instead of
vice versa); repeat the same wrapping for the other Suspense usages in this file
that lazy-load routes/components.

In `@client/src/components/related-posts.tsx`:
- Around line 19-21: The client currently fetches all posts in useEffect
(fetchPosts().then(setAllPosts).catch(() => {})) and then filters client-side,
which will scale poorly; change the component to call a server endpoint that
returns only related posts (e.g., GET /api/posts/:slug/related) instead of
fetchPosts, passing the current post slug, and set the response into setAllPosts
(or rename to setRelatedPosts) and handle errors/logging (replace the empty
catch). Update references in related-posts.tsx to use useEffect and the new
fetch path and ensure the component expects the reduced payload shape.

In `@client/src/components/series-nav.tsx`:
- Around line 29-40: The toggle button in series-nav.tsx is missing an
aria-expanded attribute so screen readers can't detect its state; update the
<button> that calls setExpanded(!expanded) to include aria-expanded tied to the
expanded state (aria-expanded={expanded}), and if the collapsible content has an
ID, also add aria-controls referencing that ID to associate the button with the
controlled region; locate the button using symbols setExpanded and expanded in
the series-nav component and add these ARIA attributes accordingly.

In `@client/src/lib/markdown.ts`:
- Around line 85-89: The range parser in client/src/lib/markdown.ts currently
assumes start <= end when handling the /^(\d+)-(\d+)$/ match inside the loop
that builds highlightLines; if a user supplies a reverse range like "{5-1}"
nothing is added. Update the logic in the block that processes rangeMatch (the
code that reads start/end and iterates to add to highlightLines) to defensively
handle reverse ranges by either swapping start and end before the for-loop or
explicitly skipping/normalizing invalid ranges so all intended lines get added;
keep the rangeMatch parsing and highlightLines.add usage but ensure start and
end are normalized before iterating.

In `@client/src/pages/admin/analytics.tsx`:
- Around line 33-39: The useEffect swallowing errors via catch(() => {}) hides
failures; replace the empty catch by capturing the error from fetchAnalytics and
setting an error state (e.g., call setError(error) or setFetchError with a
user-friendly message) and optionally clear or set data to an empty value via
setData when an error occurs; ensure setLoading(false) remains in finally. If an
error state hook doesn't exist, add a useState for error and surface it in the
UI so callers of useEffect, fetchAnalytics, setData and setLoading can show
"load failed" vs "no data".

In `@server/src/db/schema-pg.ts`:
- Around line 93-100: The visits table (pgVisits) lacks an index on the time
column (createdAt), causing scans for time-window queries; add a dedicated index
on the created_at column (e.g., idx_visits_created_at) — either by adding an
index declaration alongside the pgTable definition if your schema API supports
it or by adding a database migration that creates an index on visits.created_at
(and deploy the migration); ensure you reference the pgVisits/createdAt symbol
and run the migration so future queries filter by created_at use the new index.

In `@server/src/migrations/0006_post_versions.sql`:
- Around line 1-9: The migration creates the post_versions table but lacks an
index on post_id, causing slow WHERE post_id = ? queries; update the migration
to add an index on the post_id column (e.g., create an index named like
idx_post_versions_post_id on post_versions(post_id)) so lookups by post_id are
fast, and if your migration system supports up/down or rollback, also add the
corresponding DROP INDEX for cleanup; target symbols: table post_versions and
column post_id.

In `@server/wrangler.toml`:
- Around line 22-24: The scheduled cron under [triggers] currently runs every
minute (crons = ["* * * * *"]); update it to be environment-aware so
non-production avoids the 1-minute cadence—for example, set crons to a lower
frequency like "*/5 * * * *" or disable the trigger when ENV ≠ production;
implement the environment check where wrangler.toml is generated or read (the
[triggers] section / crons key) so production keeps the current schedule while
staging/dev use a reduced or disabled schedule.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9e3d4b5b-61c0-48dd-b16b-6e0d4a7aec60

📥 Commits

Reviewing files that changed from the base of the PR and between 0ba0401 and c24398f.

📒 Files selected for processing (29)
  • .github/dependabot.yml
  • .github/workflows/eslint.yml
  • client/package.json
  • client/src/app.tsx
  • client/src/components/post-reactions.tsx
  • client/src/components/related-posts.tsx
  • client/src/components/series-nav.tsx
  • client/src/components/share-buttons.tsx
  • client/src/env.d.ts
  • client/src/globals.css
  • client/src/lib/api.ts
  • client/src/lib/markdown.ts
  • client/src/main.tsx
  • client/src/pages/admin/analytics.tsx
  • client/src/pages/admin/dashboard.tsx
  • client/src/pages/admin/editor.tsx
  • client/src/pages/home.tsx
  • client/src/pages/post.tsx
  • client/vite.config.ts
  • server/package.json
  • server/src/db/schema-pg.ts
  • server/src/db/schema.ts
  • server/src/index.ts
  • server/src/migrations/0006_post_versions.sql
  • server/src/storage/db/d1.ts
  • server/src/storage/db/postgres.ts
  • server/src/storage/db/turso.ts
  • server/src/storage/interfaces.ts
  • server/wrangler.toml
📜 Review details
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ESLint 安全扫描
🧰 Additional context used
📓 Path-based instructions (5)
client/src/pages/**

⚙️ CodeRabbit configuration file

client/src/pages/**: 页面级组件。审查时请关注: 1. 数据加载和错误处理是否完善 2. SEO 相关(页面标题、meta 标签) 3. 导航和路由是否正确

Files:

  • client/src/pages/home.tsx
  • client/src/pages/admin/analytics.tsx
  • client/src/pages/post.tsx
  • client/src/pages/admin/dashboard.tsx
  • client/src/pages/admin/editor.tsx
client/src/components/**

⚙️ CodeRabbit configuration file

client/src/components/**: 这是 React 前端组件目录。审查时请关注: 1. 是否同时兼容暗色和亮色主题(检查 CSS 变量和 data-theme) 2. 响应式布局是否完整(移动端/平板/桌面端) 3. 无障碍访问(aria 标签、键盘导航) 4. 组件是否保持单一职责

Files:

  • client/src/components/related-posts.tsx
  • client/src/components/series-nav.tsx
  • client/src/components/post-reactions.tsx
  • client/src/components/share-buttons.tsx
client/src/globals.css

⚙️ CodeRabbit configuration file

client/src/globals.css: 全局样式和 CSS 变量系统。审查时请关注: 1. [data-theme="light"] 和默认暗色主题的变量是否配对 2. OKLCH 色值的明度/色度是否合理 3. 是否有遗漏的选择器未覆盖亮色模式

Files:

  • client/src/globals.css
server/src/index.ts

⚙️ CodeRabbit configuration file

server/src/index.ts: Hono Workers API 路由总入口。审查时请关注: 1. JWT 认证中间件是否正确保护管理接口 2. CORS 配置是否安全 3. 请求参数验证

Files:

  • server/src/index.ts
server/src/storage/**

⚙️ CodeRabbit configuration file

server/src/storage/**: 存储适配器层(数据库 + 对象存储)。审查时请关注: 1. 接口实现是否完整(IDatabase / IObjectStorage) 2. SQL 注入防护(参数化查询) 3. 多后端(D1/Turso/PostgreSQL)行为一致性 4. 错误处理和边界情况

Files:

  • server/src/storage/db/postgres.ts
  • server/src/storage/db/d1.ts
  • server/src/storage/interfaces.ts
  • server/src/storage/db/turso.ts
🪛 GitHub Check: ESLint
client/src/components/post-reactions.tsx

[warning] 69-69: Detects "variable[key]" as a left- or right-hand assignment operand.
Generic Object Injection Sink


[warning] 70-70: Detects "variable[key]" as a left- or right-hand assignment operand.
Generic Object Injection Sink

client/src/lib/markdown.ts

[warning] 124-124: Detects "variable[key]" as a left- or right-hand assignment operand.
Generic Object Injection Sink

client/src/pages/admin/dashboard.tsx

[warning] 299-299: Detects potentially unsafe regular expressions, which may take a very long time to run, blocking the event loop.
Unsafe Regular Expression

🔇 Additional comments (9)
.github/workflows/eslint.yml (1)

1-106: 安全扫描工作流整体设计合理

触发条件、最小权限、SARIF 分流上传都比较清晰,作为安全基线是加分项。

.github/dependabot.yml (1)

4-51: Dependabot 配置清晰,策略分层明确

前后端与 GitHub Actions 分开配置、节奏统一,便于长期维护依赖健康度。

client/src/env.d.ts (1)

1-2: 类型声明补充到位

为 Vite 与 PWA 客户端注入类型支持,能减少 virtual:* 模块相关的 TS 报错。

client/src/main.tsx (1)

7-16: PWA 注册逻辑正确

Service Worker 注册模式符合 vite-plugin-pwa 的推荐用法。confirm() 对话框虽简单但功能完整。

考虑后续可替换为非阻塞的 Toast/Snackbar 提示,提升用户体验,但当前实现已满足基本需求。

client/src/app.tsx (1)

8-23: 代码分割实现正确

懒加载模式使用 .then((m) => ({ default: m.XXX })) 正确处理了命名导出,所有页面组件的分割方式一致。这将显著减少初始包体积。

client/src/components/related-posts.tsx (1)

43-84: UI 实现良好

响应式布局 (grid-cols-1 sm:grid-cols-3) 正确适配移动端与桌面端。使用语义化颜色变量(foregroundmuted-foreground)确保主题兼容。卡片交互状态和过渡动画流畅。

client/src/pages/admin/analytics.tsx (1)

44-254: 整体实现结构清晰

页面布局合理,响应式网格 (grid-cols-2 sm:grid-cols-4) 适配不同屏幕。数据可视化使用百分比柱状图直观有效。空状态处理到位。

client/src/lib/markdown.ts (2)

119-135: 静态分析警告为误报

ESLint 标记的 text.split("\n")[i] 是通过循环索引安全访问,i 来自 lines.map 回调的索引参数,两个数组行数相同,不存在注入风险。

代码逻辑正确:从原始文本获取行前缀以检测 diff 的 +/- 标记,而非从已高亮的 HTML 中解析。


194-196: 懒加载图片实现完整

data-lazy-img 属性与 lazy-img 类正确配合 post.tsx 中的 IntersectionObserverglobals.css 中的淡入动画。DOMPurify 白名单更新确保属性不被过滤。

Also applies to: 239-239

Comment thread client/src/components/post-reactions.tsx
Comment thread client/src/components/post-reactions.tsx
Comment thread client/src/components/share-buttons.tsx
Comment thread client/src/globals.css Outdated
Comment thread client/src/globals.css
Comment on lines +1520 to +1529
:root:not(.dark) .reading-mode-exit-fab {
border-color: oklch(0.8 0 0 / 0.3);
background: oklch(0.98 0 0 / 0.9);
color: oklch(0.4 0 0);
}

:root:not(.dark) .reading-mode-exit-fab:hover {
background: oklch(0.95 0 0 / 0.95);
color: oklch(0.15 0 0);
border-color: oklch(0.6 0 0 / 0.3);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

这里的亮色选择器会在默认暗色下误命中。

主题系统前面统一用 [data-theme="light"] / 默认暗色变量,这里改成 :root:not(.dark) 后,默认暗色根节点同样满足条件,导致退出按钮的浅色样式覆盖暗色样式。请改回和全文件一致的 light selector。

As per coding guidelines "[data-theme="light"] 和默认暗色主题的变量是否配对".

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

In `@client/src/globals.css` around lines 1520 - 1529, The light-theme selector
:root:not(.dark) used on the .reading-mode-exit-fab rules incorrectly matches
the default dark root; change both selector occurrences back to the project's
canonical light selector [data-theme="light"] .reading-mode-exit-fab so the
light styles apply only when the light theme is active and do not override
dark-theme variables.

Comment thread server/src/storage/db/postgres.ts
Comment thread server/src/storage/db/postgres.ts
Comment thread server/src/storage/db/turso.ts
Comment thread server/src/storage/db/turso.ts
Comment thread server/src/storage/db/turso.ts Outdated
Comment thread client/src/components/post-reactions.tsx Fixed

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (5)
server/src/index.ts (3)

1208-1211: ⚠️ Potential issue | 🟡 Minor

重复的 /api/health 路由定义。

Lines 82-91 已定义详细的健康检查端点(含 dbProvider、storageProvider 等信息),但 Lines 1209-1211 又重新定义了简化版本。后者会覆盖前者,导致详细信息不可访问。

🔧 建议移除重复定义
-/* ── 健康检查 ──────────────────────────────── */
-app.get("/api/health", (c) => {
-  return c.json({ status: "ok", timestamp: new Date().toISOString() });
-});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 1208 - 1211, There is a duplicate simple
health route defined as app.get("/api/health", (c) => { ... }) that overrides
the earlier detailed health endpoint which returns dbProvider/storageProvider
and other diagnostics; remove the later simplified app.get("/api/health")
definition so the detailed health handler (the original health endpoint that
reports dbProvider, storageProvider, etc.) remains the only /api/health route,
or merge any necessary minimal fields into that detailed handler if needed.

714-741: ⚠️ Potential issue | 🟠 Major

批量图片本地化缺失超时和大小限制。

单篇文章端点 (/api/admin/posts/:slug/localize-images) 已添加 10s 超时和 10MB 限制,但批量端点 (/api/admin/localize-all-images) 未同步应用相同的安全措施,存在 SSRF 和 OOM 风险。

🛡️ 建议统一安全措施
 for (const url of externalUrls) {
   try {
-    const resp = await fetch(url, { headers: { "User-Agent": "Monolith-Bot/1.0" } });
+    const abortCtrl = new AbortController();
+    const timeoutId = setTimeout(() => abortCtrl.abort(), 10000);
+    const resp = await fetch(url, { 
+      headers: { "User-Agent": "Monolith-Bot/1.0" }, 
+      signal: abortCtrl.signal 
+    });
+    clearTimeout(timeoutId);
     if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+
+    const contentLength = resp.headers.get("content-length");
+    if (contentLength && parseInt(contentLength) > 10 * 1024 * 1024) {
+      throw new Error("图片超过 10MB 限制");
+    }

As per coding guidelines "请求参数验证"。

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

In `@server/src/index.ts` around lines 714 - 741, The batch image-localization
loop that iterates over externalUrls uses fetch (inside the for loop) without
the 10s timeout and 10MB body-size guard applied in the single-post endpoint;
update the fetch usage to use an AbortController with a 10_000ms timeout and
enforce a 10 * 1024 * 1024 byte limit when reading the response body (e.g.,
stream the response or count bytes from resp.body/arrayBuffer and abort/throw if
the limit is exceeded), ensure the ReadableStream passed to storage.put only
contains up to the allowed bytes, and keep the existing error handling around
storage.put and the replacement logic (references: externalUrls loop,
fetch(...), resp.arrayBuffer()/resp.body, ReadableStream, storage.put, content =
content.split(url).join(...)) so the batch endpoint has identical timeout/size
protections as /api/admin/posts/:slug/localize-images.

651-669: ⚠️ Potential issue | 🟠 Major

图片下载大小限制可被绕过。

当前仅检查 Content-Length 响应头,但该头可被省略或伪造。resp.arrayBuffer() 仍可能下载超限文件。建议在读取时流式检查实际大小。

🛡️ 建议使用流式读取并校验实际大小
+const MAX_SIZE = 10 * 1024 * 1024;
 const contentLength = resp.headers.get("content-length");
-if (contentLength && parseInt(contentLength) > 10 * 1024 * 1024) throw new Error("图片超过 10MB 限制");
+if (contentLength && parseInt(contentLength) > MAX_SIZE) throw new Error("图片超过 10MB 限制");

-const arrayBuf = await resp.arrayBuffer();
+// 流式读取并校验实际大小
+const reader = resp.body?.getReader();
+if (!reader) throw new Error("无法读取响应体");
+const chunks: Uint8Array[] = [];
+let totalSize = 0;
+while (true) {
+  const { done, value } = await reader.read();
+  if (done) break;
+  totalSize += value.length;
+  if (totalSize > MAX_SIZE) {
+    reader.cancel();
+    throw new Error("图片超过 10MB 限制");
+  }
+  chunks.push(value);
+}
+const arrayBuf = new Uint8Array(totalSize);
+let offset = 0;
+for (const chunk of chunks) {
+  arrayBuf.set(chunk, offset);
+  offset += chunk.length;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 651 - 669, The current download checks only
the Content-Length header and then calls resp.arrayBuffer(), which can be
bypassed; change the download to stream and enforce an actual 10MB limit by
reading from resp.body via a ReadableStream reader (use resp.body.getReader()),
accumulate chunks into a Uint8Array/ArrayBuffer up to the 10 * 1024 * 1024
limit, abort and throw if the accumulated size exceeds the limit, and only after
fully reading within limit build the final array buffer (replace the use of
resp.arrayBuffer()). Keep existing abortCtrl/timeout logic and reuse
content-type/ext/key logic after the streamed read completes (referencing resp,
abortCtrl, contentLength, arrayBuf/key replacement).
server/src/storage/db/postgres.ts (1)

102-111: ⚠️ Potential issue | 🔴 Critical

语法错误:comments 表创建语句缺少 await this.client 前缀。

Line 102 的 SQL 语句未被正确包裹执行,这会导致 TypeScript 解析失败。静态分析工具已标记此问题。

🐛 修复语法错误
     `;
+    await this.client`
       CREATE TABLE IF NOT EXISTS comments (
         id SERIAL PRIMARY KEY,
         post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
         author_name TEXT NOT NULL,
         author_email TEXT NOT NULL DEFAULT '',
         content TEXT NOT NULL,
         approved BOOLEAN NOT NULL DEFAULT false,
         created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
       )
     `;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/storage/db/postgres.ts` around lines 102 - 111, The CREATE TABLE
SQL for comments is written as a bare template string and not executed; wrap the
SQL in a call on the Postgres client and await it (e.g., use await
this.client.query(...)) inside the method that runs migrations/initialization
(look for the init/createTables or similar function in
server/src/storage/db/postgres.ts) so the statement is actually executed and
TypeScript parsing passes.
server/src/storage/db/d1.ts (1)

29-44: ⚠️ Potential issue | 🔴 Critical

缺少 postVersions 表初始化。

ensureSchema() 未调用 post_versions 表的创建逻辑,但 getPostVersionscreatePostVersionrestorePostVersion 直接使用该表。新 D1 数据库首次调用版本历史功能时会报错。

🐛 建议添加初始化方法
+  private async ensurePostVersionsTable(): Promise<void> {
+    await this.db.run(sql`CREATE TABLE IF NOT EXISTS post_versions (
+      id INTEGER PRIMARY KEY AUTOINCREMENT,
+      post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
+      title TEXT NOT NULL,
+      content TEXT NOT NULL,
+      excerpt TEXT DEFAULT '',
+      created_at TEXT NOT NULL DEFAULT (datetime('now'))
+    )`);
+  }

   async ensureSchema(): Promise<void> {
     if (!this.schemaReady) {
       this.schemaReady = (async () => {
         await this.ensureViewCountColumn();
         await this.ensurePinnedColumn();
         await this.ensureSettingsTable();
         await this.ensurePagesTable();
         await this.ensureCommentsTable();
         await this.ensureSeriesColumns();
         await this.ensureReactionsTable();
         await this.ensureVisitsTable();
         await this.ensureCategoryColumn();
+        await this.ensurePostVersionsTable();
       })();
     }
     await this.schemaReady;
   }

As per coding guidelines "server/src/storage/** 审查需关注:3. 多后端(D1/Turso/PostgreSQL)行为一致性"。

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

In `@server/src/storage/db/d1.ts` around lines 29 - 44, ensureSchema() doesn't
initialize the post_versions table, causing runtime errors when
getPostVersions/createPostVersion/restorePostVersion access it; add a new
initialization routine (e.g., ensurePostVersionsTable) that creates the
post_versions table/schema for D1 and call it from ensureSchema() alongside the
other ensure* calls, and implement the table-creation SQL inside
ensurePostVersionsTable so the post version functions can assume the table
exists.
🧹 Nitpick comments (7)
client/src/components/post-reactions.tsx (1)

4-9: 建议收窄 reaction 类型,提升类型安全并减少动态索引风险噪音。

counts 目前是 Record<string, number>,配合 counts[type] 会放大“任意 key”空间。将 type 收窄为字面量联合后,读写更安全,也更利于静态分析。

♻️ 建议改造(类型收窄)
-const REACTION_TYPES = [
+const REACTION_TYPES = [
   { type: "like", emoji: "👍", label: "赞" },
   { type: "heart", emoji: "❤️", label: "喜欢" },
   { type: "celebrate", emoji: "🎉", label: "庆祝" },
   { type: "think", emoji: "🤔", label: "值得思考" },
-];
+] as const;
+
+type ReactionType = (typeof REACTION_TYPES)[number]["type"];
@@
-  const [counts, setCounts] = useState<Record<string, number>>({});
+  const [counts, setCounts] = useState<Partial<Record<ReactionType, number>>>({});
@@
-  const handleReaction = async (type: string) => {
+  const handleReaction = async (type: ReactionType) => {

Also applies to: 16-16, 64-64, 83-84

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

In `@client/src/components/post-reactions.tsx` around lines 4 - 9, The
REACTION_TYPES array should be tied to a narrowed literal union so accesses are
type-safe: add a ReactionType union (e.g. 'like'|'heart'|'celebrate'|'think')
and use it as the element type for REACTION_TYPES and as the key type for counts
(replace Record<string, number> with Record<ReactionType, number> or
Partial<Record<ReactionType, number>>), then update any places that index counts
(uses around REACTION_TYPES, the component state/props and handlers that
read/write counts) to accept ReactionType instead of string so TypeScript
prevents invalid dynamic keys and improves static checks. Ensure
function/variable names referenced: REACTION_TYPES and counts, plus any handler
props that accept a reaction type, are updated to the new ReactionType union.
server/src/index.ts (2)

10-10: 未使用的 marked 导入。

marked 已导入但在当前文件中未被引用,建议移除以保持代码整洁。

🧹 建议移除
-import { marked } from "marked";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` at line 10, The import "marked" is unused in
server/src/index.ts; remove the unused import statement (the `import { marked }
from "marked";` line) to clean up the module and avoid unused-import warnings;
if future code needs markdown parsing, re-add the import where it's actually
used (look for the top-level import of `marked` to remove).

62-80: Webhook 函数类型签名可强化,非边缘环境下静默丢弃 Promise。

  1. c: anypayload: any 可使用更精确的类型
  2. Line 78 的 Promise.allSettled(promises) 在非边缘环境下是一个悬空 Promise,错误信息会丢失
♻️ 建议改进
-async function triggerWebhook(c: any, eventName: string, payload: any) {
+async function triggerWebhook(
+  c: { env: Bindings; executionCtx?: { waitUntil: (p: Promise<unknown>) => void } },
+  eventName: string,
+  payload: unknown
+) {
   if (!c.env.WEBHOOK_URLS) return;
   // ...
   if (c.executionCtx && c.executionCtx.waitUntil) {
     c.executionCtx.waitUntil(Promise.allSettled(promises));
   } else {
-    Promise.allSettled(promises);
+    // 非边缘环境下仍需等待或记录
+    Promise.allSettled(promises).catch(() => {});
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/index.ts` around lines 62 - 80, The triggerWebhook function uses
loose types and leaves a dangling Promise when executionCtx.waitUntil is absent;
update its signature (triggerWebhook) to use precise types for c (e.g. c: { env:
{ WEBHOOK_URLS?: string }, executionCtx?: { waitUntil?: (p: Promise<unknown>) =>
void } }) and payload: unknown (or a specific payload type), then ensure the
promises are handled: when c.executionCtx?.waitUntil exists call it with
Promise.allSettled(promises), otherwise await Promise.allSettled(promises) (or
return it) so failures are observed and logged; keep the existing per-request
.catch logging for fetch errors and ensure the final allSettled result is not
discarded.
server/src/storage/db/postgres.ts (3)

991-1000: getAnalytics 中三重冗余建表。

已调用 ensureCoreTables() 后又内联 CREATE TABLE visits,而 recordVisit() 也有相同逻辑。建议移除冗余代码。

🧹 建议移除冗余
 async getAnalytics(days: number) {
   await this.ensureCoreTables();
-  await this.client`
-    CREATE TABLE IF NOT EXISTS visits (
-      id SERIAL PRIMARY KEY, path TEXT NOT NULL,
-      country TEXT NOT NULL DEFAULT 'XX', referer_domain TEXT NOT NULL DEFAULT '',
-      device_type TEXT NOT NULL DEFAULT 'desktop',
-      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
-    )
-  `;
   const byDay = await this.client`SELECT DATE(created_at) as date, COUNT(*)::int as count FROM visits WHERE created_at >= NOW() - INTERVAL '1 day' * ${days} GROUP BY DATE(created_at) ORDER BY date`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/storage/db/postgres.ts` around lines 991 - 1000, getAnalytics
contains an inline CREATE TABLE for visits that duplicates ensureCoreTables and
the same logic in recordVisit; remove the inline CREATE TABLE block from
getAnalytics and rely on ensureCoreTables to create the visits table, and
similarly eliminate the duplicate CREATE TABLE logic in recordVisit if present
so table creation is centralized in ensureCoreTables (referencing
ensureCoreTables, getAnalytics, recordVisit, and the visits table to locate the
code).

979-989: recordVisit 内联建表冗余。

toggleReaction 相同的问题,建议统一调用 ensureCoreTables() 而非内联 CREATE TABLE

♻️ 建议简化
 async recordVisit(data: { path: string; country: string; refererDomain: string; deviceType: string }): Promise<void> {
-  await this.client`
-    CREATE TABLE IF NOT EXISTS visits (
-      id SERIAL PRIMARY KEY, path TEXT NOT NULL,
-      country TEXT NOT NULL DEFAULT 'XX', referer_domain TEXT NOT NULL DEFAULT '',
-      device_type TEXT NOT NULL DEFAULT 'desktop',
-      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
-    )
-  `;
+  await this.ensureCoreTables();
   await this.client`INSERT INTO visits (path, country, referer_domain, device_type) VALUES (${data.path}, ${data.country}, ${data.refererDomain}, ${data.deviceType})`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/storage/db/postgres.ts` around lines 979 - 989, The recordVisit
method currently runs an inline CREATE TABLE; remove that redundant CREATE TABLE
block and instead call the shared initializer method (ensureCoreTables) like
toggleReaction does: inside recordVisit (function name recordVisit) invoke await
this.ensureCoreTables() before performing the INSERT, then run the INSERT
statement only; ensure you reference the same ensureCoreTables implementation
used by toggleReaction to avoid duplicating schema creation logic.

954-977: toggleReaction 内联建表与 ensureCoreTables 定义不一致。

Line 956-965 的内联 CREATE TABLE 缺少 REFERENCES posts(slug) ON DELETE CASCADE 外键约束,而 ensureCoreTables() (Lines 122-131) 包含该约束。建议移除冗余的内联建表或保持一致。

♻️ 建议统一调用 ensureCoreTables
 async toggleReaction(postSlug: string, type: string, ipHash: string): Promise<{ action: "added" | "removed" }> {
-  // 确保表存在
-  await this.client`
-    CREATE TABLE IF NOT EXISTS reactions (
-      id SERIAL PRIMARY KEY,
-      post_slug TEXT NOT NULL,
-      type TEXT NOT NULL,
-      ip_hash TEXT NOT NULL,
-      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-      UNIQUE(post_slug, type, ip_hash)
-    )
-  `;
+  await this.ensureCoreTables();
   const [existing] = await this.client`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/storage/db/postgres.ts` around lines 954 - 977, Remove the inline
CREATE TABLE in toggleReaction and instead ensure the canonical schema is used
by calling the existing ensureCoreTables() helper at the start of toggleReaction
so the reactions table is created with the same REFERENCES posts(slug) ON DELETE
CASCADE constraint as defined in ensureCoreTables(); locate the CREATE TABLE
block inside toggleReaction and delete it, then add a call to
this.ensureCoreTables() (or the appropriate initialization method) before
querying/inserting into reactions to keep schema definitions consistent.
server/src/storage/db/d1.ts (1)

965-985: getAnalytics 参数处理已改进,但仍有优化空间。

当前实现通过 parseInt 规范化 days 并构造 sinceMod 字符串,然后作为参数传入 sql\`。虽然 Drizzle 的 sql模板会对插值进行参数绑定,但直接拼接-${days} days` 字符串依赖上游验证。建议进一步强化防护。

🛡️ 建议强化类型校验
 async getAnalytics(daysInput: number) {
   await this.ensureVisitsTable();
-  let days = Math.max(0, parseInt(String(daysInput), 10));
-  if (isNaN(days)) days = 7;
+  let days = Number.isFinite(daysInput) && daysInput > 0 
+    ? Math.min(Math.floor(daysInput), 365) 
+    : 7;
   const sinceMod = `-${days} days`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/storage/db/d1.ts` around lines 965 - 985, The getAnalytics method
currently builds sinceMod = `-${days} days` and interpolates it into sql, which
risks relying on upstream validation; instead, validate and clamp daysInput to a
safe integer range (e.g., 0–365), ensure parseInt result is not NaN (fallback to
default like 7), then compute an explicit cutoff timestamp in JS (e.g.,
Date.now() - days*24*60*60*1000) and pass that ISO/timestamp as a bound
parameter to db.run queries (replace uses of sinceMod in sql calls). Update the
logic in getAnalytics (referencing daysInput, days, sinceMod, and the db.run/sql
calls) so all queries use a parameterized cutoff value rather than embedding the
modifier string.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/src/components/post-reactions.tsx`:
- Around line 21-28: When slug changes we must clear per-article click state and
guard against stale async results: in the useEffect that calls
fetchReactions(slug) and reads localStorage (`useEffect` block around
fetchReactions and setActiveTypes), reset activeTypes to an empty Set
immediately before loading saved state (e.g., setActiveTypes(new Set())) so
older article flags don't persist when no saved entry exists; also scope async
responses by capturing the current slug (or using an AbortController) so that
fetchReactions results only call setCounts when the response's slug matches the
current slug, and likewise ensure toggleReaction verifies the action's slug
matches the current slug before applying state updates or localStorage writes.

---

Outside diff comments:
In `@server/src/index.ts`:
- Around line 1208-1211: There is a duplicate simple health route defined as
app.get("/api/health", (c) => { ... }) that overrides the earlier detailed
health endpoint which returns dbProvider/storageProvider and other diagnostics;
remove the later simplified app.get("/api/health") definition so the detailed
health handler (the original health endpoint that reports dbProvider,
storageProvider, etc.) remains the only /api/health route, or merge any
necessary minimal fields into that detailed handler if needed.
- Around line 714-741: The batch image-localization loop that iterates over
externalUrls uses fetch (inside the for loop) without the 10s timeout and 10MB
body-size guard applied in the single-post endpoint; update the fetch usage to
use an AbortController with a 10_000ms timeout and enforce a 10 * 1024 * 1024
byte limit when reading the response body (e.g., stream the response or count
bytes from resp.body/arrayBuffer and abort/throw if the limit is exceeded),
ensure the ReadableStream passed to storage.put only contains up to the allowed
bytes, and keep the existing error handling around storage.put and the
replacement logic (references: externalUrls loop, fetch(...),
resp.arrayBuffer()/resp.body, ReadableStream, storage.put, content =
content.split(url).join(...)) so the batch endpoint has identical timeout/size
protections as /api/admin/posts/:slug/localize-images.
- Around line 651-669: The current download checks only the Content-Length
header and then calls resp.arrayBuffer(), which can be bypassed; change the
download to stream and enforce an actual 10MB limit by reading from resp.body
via a ReadableStream reader (use resp.body.getReader()), accumulate chunks into
a Uint8Array/ArrayBuffer up to the 10 * 1024 * 1024 limit, abort and throw if
the accumulated size exceeds the limit, and only after fully reading within
limit build the final array buffer (replace the use of resp.arrayBuffer()). Keep
existing abortCtrl/timeout logic and reuse content-type/ext/key logic after the
streamed read completes (referencing resp, abortCtrl, contentLength,
arrayBuf/key replacement).

In `@server/src/storage/db/d1.ts`:
- Around line 29-44: ensureSchema() doesn't initialize the post_versions table,
causing runtime errors when getPostVersions/createPostVersion/restorePostVersion
access it; add a new initialization routine (e.g., ensurePostVersionsTable) that
creates the post_versions table/schema for D1 and call it from ensureSchema()
alongside the other ensure* calls, and implement the table-creation SQL inside
ensurePostVersionsTable so the post version functions can assume the table
exists.

In `@server/src/storage/db/postgres.ts`:
- Around line 102-111: The CREATE TABLE SQL for comments is written as a bare
template string and not executed; wrap the SQL in a call on the Postgres client
and await it (e.g., use await this.client.query(...)) inside the method that
runs migrations/initialization (look for the init/createTables or similar
function in server/src/storage/db/postgres.ts) so the statement is actually
executed and TypeScript parsing passes.

---

Nitpick comments:
In `@client/src/components/post-reactions.tsx`:
- Around line 4-9: The REACTION_TYPES array should be tied to a narrowed literal
union so accesses are type-safe: add a ReactionType union (e.g.
'like'|'heart'|'celebrate'|'think') and use it as the element type for
REACTION_TYPES and as the key type for counts (replace Record<string, number>
with Record<ReactionType, number> or Partial<Record<ReactionType, number>>),
then update any places that index counts (uses around REACTION_TYPES, the
component state/props and handlers that read/write counts) to accept
ReactionType instead of string so TypeScript prevents invalid dynamic keys and
improves static checks. Ensure function/variable names referenced:
REACTION_TYPES and counts, plus any handler props that accept a reaction type,
are updated to the new ReactionType union.

In `@server/src/index.ts`:
- Line 10: The import "marked" is unused in server/src/index.ts; remove the
unused import statement (the `import { marked } from "marked";` line) to clean
up the module and avoid unused-import warnings; if future code needs markdown
parsing, re-add the import where it's actually used (look for the top-level
import of `marked` to remove).
- Around line 62-80: The triggerWebhook function uses loose types and leaves a
dangling Promise when executionCtx.waitUntil is absent; update its signature
(triggerWebhook) to use precise types for c (e.g. c: { env: { WEBHOOK_URLS?:
string }, executionCtx?: { waitUntil?: (p: Promise<unknown>) => void } }) and
payload: unknown (or a specific payload type), then ensure the promises are
handled: when c.executionCtx?.waitUntil exists call it with
Promise.allSettled(promises), otherwise await Promise.allSettled(promises) (or
return it) so failures are observed and logged; keep the existing per-request
.catch logging for fetch errors and ensure the final allSettled result is not
discarded.

In `@server/src/storage/db/d1.ts`:
- Around line 965-985: The getAnalytics method currently builds sinceMod =
`-${days} days` and interpolates it into sql, which risks relying on upstream
validation; instead, validate and clamp daysInput to a safe integer range (e.g.,
0–365), ensure parseInt result is not NaN (fallback to default like 7), then
compute an explicit cutoff timestamp in JS (e.g., Date.now() -
days*24*60*60*1000) and pass that ISO/timestamp as a bound parameter to db.run
queries (replace uses of sinceMod in sql calls). Update the logic in
getAnalytics (referencing daysInput, days, sinceMod, and the db.run/sql calls)
so all queries use a parameterized cutoff value rather than embedding the
modifier string.

In `@server/src/storage/db/postgres.ts`:
- Around line 991-1000: getAnalytics contains an inline CREATE TABLE for visits
that duplicates ensureCoreTables and the same logic in recordVisit; remove the
inline CREATE TABLE block from getAnalytics and rely on ensureCoreTables to
create the visits table, and similarly eliminate the duplicate CREATE TABLE
logic in recordVisit if present so table creation is centralized in
ensureCoreTables (referencing ensureCoreTables, getAnalytics, recordVisit, and
the visits table to locate the code).
- Around line 979-989: The recordVisit method currently runs an inline CREATE
TABLE; remove that redundant CREATE TABLE block and instead call the shared
initializer method (ensureCoreTables) like toggleReaction does: inside
recordVisit (function name recordVisit) invoke await this.ensureCoreTables()
before performing the INSERT, then run the INSERT statement only; ensure you
reference the same ensureCoreTables implementation used by toggleReaction to
avoid duplicating schema creation logic.
- Around line 954-977: Remove the inline CREATE TABLE in toggleReaction and
instead ensure the canonical schema is used by calling the existing
ensureCoreTables() helper at the start of toggleReaction so the reactions table
is created with the same REFERENCES posts(slug) ON DELETE CASCADE constraint as
defined in ensureCoreTables(); locate the CREATE TABLE block inside
toggleReaction and delete it, then add a call to this.ensureCoreTables() (or the
appropriate initialization method) before querying/inserting into reactions to
keep schema definitions consistent.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2f2eeed5-7ba0-4a5d-922c-4d4e3cd567b7

📥 Commits

Reviewing files that changed from the base of the PR and between c24398f and 4b60c65.

⛔ Files ignored due to path filters (3)
  • client/public/favicon.png is excluded by !**/*.png
  • client/public/icon-192.png is excluded by !**/*.png
  • client/public/icon-512.png is excluded by !**/*.png
📒 Files selected for processing (7)
  • client/src/components/post-reactions.tsx
  • server/src/db/schema-pg.ts
  • server/src/db/schema.ts
  • server/src/index.ts
  • server/src/storage/db/d1.ts
  • server/src/storage/db/postgres.ts
  • server/src/storage/db/turso.ts
✅ Files skipped from review due to trivial changes (1)
  • server/src/storage/db/turso.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ESLint 安全扫描
  • GitHub Check: ESLint 安全扫描
🧰 Additional context used
📓 Path-based instructions (3)
server/src/index.ts

⚙️ CodeRabbit configuration file

server/src/index.ts: Hono Workers API 路由总入口。审查时请关注: 1. JWT 认证中间件是否正确保护管理接口 2. CORS 配置是否安全 3. 请求参数验证

Files:

  • server/src/index.ts
server/src/storage/**

⚙️ CodeRabbit configuration file

server/src/storage/**: 存储适配器层(数据库 + 对象存储)。审查时请关注: 1. 接口实现是否完整(IDatabase / IObjectStorage) 2. SQL 注入防护(参数化查询) 3. 多后端(D1/Turso/PostgreSQL)行为一致性 4. 错误处理和边界情况

Files:

  • server/src/storage/db/d1.ts
  • server/src/storage/db/postgres.ts
client/src/components/**

⚙️ CodeRabbit configuration file

client/src/components/**: 这是 React 前端组件目录。审查时请关注: 1. 是否同时兼容暗色和亮色主题(检查 CSS 变量和 data-theme) 2. 响应式布局是否完整(移动端/平板/桌面端) 3. 无障碍访问(aria 标签、键盘导航) 4. 组件是否保持单一职责

Files:

  • client/src/components/post-reactions.tsx
🪛 Biome (2.4.10)
server/src/storage/db/postgres.ts

[error] 113-113: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 113-113: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 113-113: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 113-113: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 113-113: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 114-114: expected , but instead found SERIAL

(parse)


[error] 114-114: expected , but instead found PRIMARY

(parse)


[error] 114-114: expected , but instead found KEY

(parse)


[error] 115-115: expected , but instead found INTEGER

(parse)


[error] 115-115: expected , but instead found NOT

(parse)


[error] 115-115: expected , but instead found NULL

(parse)


[error] 115-115: expected , but instead found REFERENCES

(parse)


[error] 115-115: expected , but instead found posts

(parse)


[error] 115-115: expected , but instead found ON

(parse)


[error] 115-115: expected , but instead found DELETE

(parse)


[error] 115-115: expected , but instead found CASCADE

(parse)


[error] 116-116: expected , but instead found TEXT

(parse)


[error] 116-116: expected , but instead found NOT

(parse)


[error] 116-116: expected , but instead found NULL

(parse)


[error] 117-117: expected , but instead found TEXT

(parse)


[error] 117-117: expected , but instead found NOT

(parse)


[error] 117-117: expected , but instead found NULL

(parse)


[error] 118-118: expected , but instead found TEXT

(parse)


[error] 119-119: expected , but instead found TIMESTAMPTZ

(parse)


[error] 119-119: expected , but instead found NOT

(parse)


[error] 119-119: expected , but instead found NULL

(parse)


[error] 119-119: expected , but instead found DEFAULT

(parse)


[error] 119-119: expected , but instead found NOW

(parse)


[error] 123-123: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 123-123: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 123-123: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 123-123: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 123-123: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 124-124: expected , but instead found SERIAL

(parse)


[error] 124-124: expected , but instead found PRIMARY

(parse)


[error] 124-124: expected , but instead found KEY

(parse)


[error] 125-125: expected , but instead found TEXT

(parse)


[error] 125-125: expected , but instead found NOT

(parse)


[error] 125-125: expected , but instead found NULL

(parse)


[error] 125-125: expected , but instead found REFERENCES

(parse)


[error] 125-125: expected , but instead found posts

(parse)


[error] 125-125: expected , but instead found ON

(parse)


[error] 125-125: expected , but instead found DELETE

(parse)


[error] 125-125: expected , but instead found CASCADE

(parse)


[error] 126-126: expected , but instead found TEXT

(parse)


[error] 126-126: expected , but instead found NOT

(parse)


[error] 126-126: expected , but instead found NULL

(parse)


[error] 127-127: expected , but instead found TEXT

(parse)


[error] 127-127: expected , but instead found NOT

(parse)


[error] 127-127: expected , but instead found NULL

(parse)


[error] 128-128: expected , but instead found TIMESTAMPTZ

(parse)


[error] 128-128: expected , but instead found NOT

(parse)


[error] 128-128: expected , but instead found NULL

(parse)


[error] 128-128: expected , but instead found DEFAULT

(parse)


[error] 128-128: expected , but instead found NOW

(parse)


[error] 133-133: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 133-133: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 133-133: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 133-133: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 133-133: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 134-134: expected , but instead found SERIAL

(parse)


[error] 134-134: expected , but instead found PRIMARY

(parse)


[error] 134-134: expected , but instead found KEY

(parse)


[error] 135-135: expected , but instead found TEXT

(parse)


[error] 135-135: expected , but instead found NOT

(parse)


[error] 135-135: expected , but instead found NULL

(parse)


[error] 136-136: expected , but instead found TEXT

(parse)


[error] 136-136: expected , but instead found NOT

(parse)


[error] 136-136: expected , but instead found NULL

(parse)


[error] 136-136: expected , but instead found DEFAULT

(parse)


[error] 136-136: expected , but instead found 'XX'

(parse)


[error] 137-137: expected , but instead found TEXT

(parse)


[error] 137-137: expected , but instead found NOT

(parse)


[error] 137-137: expected , but instead found NULL

(parse)


[error] 137-137: expected , but instead found DEFAULT

(parse)


[error] 137-137: expected , but instead found ''

(parse)


[error] 138-138: expected , but instead found TEXT

(parse)


[error] 138-138: expected , but instead found NOT

(parse)


[error] 138-138: expected , but instead found NULL

(parse)


[error] 138-138: expected , but instead found DEFAULT

(parse)


[error] 138-138: expected , but instead found 'desktop'

(parse)


[error] 139-139: expected , but instead found TIMESTAMPTZ

(parse)


[error] 139-139: expected , but instead found NOT

(parse)


[error] 139-139: expected , but instead found NULL

(parse)


[error] 139-139: expected , but instead found DEFAULT

(parse)


[error] 139-139: expected , but instead found NOW

(parse)

🪛 GitHub Check: ESLint
client/src/components/post-reactions.tsx

[warning] 64-64: Detects "variable[key]" as a left- or right-hand assignment operand.
Generic Object Injection Sink


[warning] 83-83: Detects "variable[key]" as a left- or right-hand assignment operand.
Generic Object Injection Sink


[warning] 84-84: Detects "variable[key]" as a left- or right-hand assignment operand.
Generic Object Injection Sink

🔇 Additional comments (15)
client/src/components/post-reactions.tsx (1)

73-80: 按钮无障碍语义与并发交互控制实现良好。

aria-labelaria-pressed 以及 disabled={inFlight.has(type)} 的组合是正确方向,读屏与防重复点击都覆盖到了。

As per coding guidelines "无障碍访问(aria 标签、键盘导航)".

server/src/db/schema.ts (3)

82-95: reactions 表约束已完善,符合要求。

当前实现已通过 references(() => posts.slug, { onDelete: "cascade" }) 建立外键约束,并添加了 uniqueIndex 防止重复计数。相比之前的 review 建议,虽然继续使用 postSlug 而非 postId,但在功能层面已解决孤儿数据和重复写入风险。


97-107: visits 表结构合理。

访客记录表设计正确,包含路径、地区、来源域名、设备类型等分析所需字段,默认值设置得当。


109-121: postVersions 表设计合理。

版本历史表通过 postId 外键关联文章,配合 onDelete: cascade 确保文章删除时自动清理历史记录。

server/src/index.ts (3)

580-593: 批量操作接口参数验证已完善。

action 参数现已通过 ["publish", "unpublish", "delete"].includes(action) 进行运行时校验,slugs 数组也有非空检查。符合 coding guidelines 中关于"请求参数验证"的要求。


563-578: 版本恢复顺序已修正。

现在先调用 createPostVersion(slug) 保存当前状态快照,再执行 restorePostVersion(slug, versionId),保留了撤销恢复的能力。


1213-1222: 定时任务导出结构正确。

scheduled 处理函数正确实例化数据库并调用 publishScheduledPosts(),配合 wrangler.toml 中的 cron 触发器使用。

server/src/db/schema-pg.ts (2)

82-94: pgReactions 约束已完善。

PostgreSQL 版本的 reactions 表现已包含 postSlug 外键约束和 uniqueIndex 复合唯一索引,与 SQLite 版本保持一致。


96-116: pgVisits 和 pgPostVersions 表结构正确。

两表结构与 SQLite 版本对应,类型转换正确(使用 timestamp 替代 textserial 替代 integer autoincrement)。

server/src/storage/db/postgres.ts (3)

61-141: ensureCoreTables 已补齐新表和新列。

post_versionsreactionsvisits 表以及 series_slugcategoryseries_order 列的创建/补充逻辑已添加,解决了之前 review 指出的初始化遗漏问题。


922-938: getSeriesPosts 和 getCategories 已添加定时发布过滤。

两个查询现已包含 (publish_at IS NULL OR publish_at <= NOW()) 条件,与 getPublishedPosts() 保持一致,避免暴露未来定时文章。


940-952: getReactions 已添加表初始化检查。

通过先调用 ensureCoreTables() 确保 reactions 表存在,解决了之前 review 指出的新库首次读取失败问题。

server/src/storage/db/d1.ts (3)

887-902: getSeriesPosts 和 getCategories 已添加定时发布过滤。

D1 适配器中两个查询现已包含 datetime('now') 过滤条件,与其他后端保持一致。


904-942: reactions 相关方法实现正确。

ensureReactionsTable 创建带唯一约束的表,getReactionstoggleReaction 正确处理查询和切换逻辑。


344-369: 批量操作和定时发布方法实现正确。

batchOperatePosts 使用 inArray 进行批量操作,publishScheduledPosts 正确筛选待发布文章。与 PostgreSQL 适配器行为一致。

Comment thread client/src/components/post-reactions.tsx

// diff 高亮:检测原始文本行前缀
// eslint-disable-next-line security/detect-object-injection
const rawLine = rawLines[i] || "";

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
client/src/components/post-reactions.tsx (1)

73-75: 异常分支不建议完全静默。

当前 catch 为空会丢失排障信号;建议至少做受控日志或埋点,避免线上问题难定位。

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

In `@client/src/components/post-reactions.tsx` around lines 73 - 75, The empty
catch in post-reactions.tsx should not swallow errors; change the bare "catch"
to "catch (err)" and log or report the error from the PostReactions async
handler (the try/catch around the reaction update) using your project's
telemetry or a concise console.error call (e.g. console.error("PostReactions:
failed to update reaction", err) or reportError(err, { context:
"PostReactions.update" })) so failures are observable while keeping behavior
unchanged.
client/src/pages/admin/dashboard.tsx (1)

106-107: 避免 catch (err: any),保持错误分支类型安全

Line 106 使用 any 会绕过类型检查,建议改为 unknown 并做类型收窄。

建议改法
- } catch (err: any) {
-   alert(err.message || "批量操作失败");
+ } catch (err: unknown) {
+   const message = err instanceof Error ? err.message : "批量操作失败";
+   alert(message);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/admin/dashboard.tsx` around lines 106 - 107, Replace the
loose catch typing and direct property access by using unknown and narrowing
before reading message: change the catch clause that currently reads catch (err:
any) { alert(err.message || "批量操作失败"); } to catch (err: unknown) and then narrow
(e.g., if (err instanceof Error) use err.message, else use String(err) or a
default "批量操作失败") so you safely handle non-Error throwables; update the catch
block around the bulk operation in dashboard.tsx where alert(...) is called.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@client/src/components/post-reactions.tsx`:
- Around line 52-54: The animation state has a race because the unqualified
setTimeout callback clears animating regardless of newer clicks; modify the
click handler in the post-reactions component so you cancel any existing timeout
before starting a new one (use a module-level or component-level ref like
timerRef via useRef), then setAnimating(type) and assign the new timeout id to
timerRef so its callback clears animating only for the most recent timer; also
clear the timerRef in the component cleanup to avoid leaks.

In `@client/src/components/share-buttons.tsx`:
- Around line 27-31: The copy handler's setTimeouts can overlap causing flicker;
in handleCopy, reuse a single timer by storing the timeout id in a persistent
ref (e.g., copyTimerRef using useRef<number | null>), call
clearTimeout(copyTimerRef.current) before creating a new setTimeout, assign the
new id to copyTimerRef.current, and clear it on unmount (useEffect cleanup) so
setCopied(true) / setCopied(false) cannot be preempted by earlier timers.

In `@client/src/pages/admin/dashboard.tsx`:
- Around line 349-354: The hardcoded SEO status array (the anonymous array
mapped in the JSX with items like "Sitemap", "Robots noindex (404)", "JSON-LD
结构化", "OG 社交标签" and property ok: true) must not always report success; replace
the static ok:true with real detection or an explicit undetected state:
implement or call a helper (e.g., getSeoStatus or checkSeoStatus) to compute
each item's status (or use a placeholder like status: "unknown"/null while async
detection runs) and render that result instead of the permanent ok:true so the
Dashboard component shows true health only when verified.
- Around line 262-264: The row-level select button (toggleSelect, selectedSlugs,
post.slug) only renders an icon (CheckSquare/Square) and lacks accessible
semantics; update the button to expose state and target by adding an
aria-pressed attribute bound to selectedSlugs.has(post.slug) and a clear
aria-label (e.g. `${selected ? 'Deselect' : 'Select'} article ${post.title ||
post.slug}`) so screen readers know which article and whether it is selected,
and keep the existing onClick calling toggleSelect(post.slug); you can also add
a title attribute with the same text for sighted tooltips.
- Around line 84-89: The toggleSelect function currently mutates state using a
closed-over selectedSlugs and should use a functional state update like
handleDelete does; change toggleSelect (the handler that calls setSelectedSlugs)
to call setSelectedSlugs(prev => { create a new Set from prev, add or delete the
slug accordingly, and return that new Set }) so updates are based on the latest
state and avoid lost updates on rapid clicks.

---

Nitpick comments:
In `@client/src/components/post-reactions.tsx`:
- Around line 73-75: The empty catch in post-reactions.tsx should not swallow
errors; change the bare "catch" to "catch (err)" and log or report the error
from the PostReactions async handler (the try/catch around the reaction update)
using your project's telemetry or a concise console.error call (e.g.
console.error("PostReactions: failed to update reaction", err) or
reportError(err, { context: "PostReactions.update" })) so failures are
observable while keeping behavior unchanged.

In `@client/src/pages/admin/dashboard.tsx`:
- Around line 106-107: Replace the loose catch typing and direct property access
by using unknown and narrowing before reading message: change the catch clause
that currently reads catch (err: any) { alert(err.message || "批量操作失败"); } to
catch (err: unknown) and then narrow (e.g., if (err instanceof Error) use
err.message, else use String(err) or a default "批量操作失败") so you safely handle
non-Error throwables; update the catch block around the bulk operation in
dashboard.tsx where alert(...) is called.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bc121253-67da-4dff-bf32-162693598d39

📥 Commits

Reviewing files that changed from the base of the PR and between 4b60c65 and 89141ea.

📒 Files selected for processing (9)
  • client/src/components/footer.tsx
  • client/src/components/navbar.tsx
  • client/src/components/post-reactions.tsx
  • client/src/components/share-buttons.tsx
  • client/src/globals.css
  • client/src/lib/markdown.ts
  • client/src/pages/admin/dashboard.tsx
  • client/vite.config.ts
  • server/src/storage/db/postgres.ts
✅ Files skipped from review due to trivial changes (4)
  • client/src/components/footer.tsx
  • client/src/components/navbar.tsx
  • client/src/globals.css
  • server/src/storage/db/postgres.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • client/vite.config.ts
  • client/src/lib/markdown.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: ESLint 安全扫描
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: ESLint 安全扫描
🧰 Additional context used
📓 Path-based instructions (2)
client/src/components/**

⚙️ CodeRabbit configuration file

client/src/components/**: 这是 React 前端组件目录。审查时请关注: 1. 是否同时兼容暗色和亮色主题(检查 CSS 变量和 data-theme) 2. 响应式布局是否完整(移动端/平板/桌面端) 3. 无障碍访问(aria 标签、键盘导航) 4. 组件是否保持单一职责

Files:

  • client/src/components/share-buttons.tsx
  • client/src/components/post-reactions.tsx
client/src/pages/**

⚙️ CodeRabbit configuration file

client/src/pages/**: 页面级组件。审查时请关注: 1. 数据加载和错误处理是否完善 2. SEO 相关(页面标题、meta 标签) 3. 导航和路由是否正确

Files:

  • client/src/pages/admin/dashboard.tsx
🔇 Additional comments (3)
client/src/components/post-reactions.tsx (2)

22-45: Slug 切换后的状态隔离与竞态保护实现到位。

这里对 counts/activeTypes 的显式重置,以及异步结果的 slug 归属校验,能有效避免跨文章状态串写。


93-100: 按钮可访问性语义完整,交互状态暴露正确。

aria-labelaria-presseddisabled 的组合实现合理。

As per coding guidelines "无障碍访问(aria 标签、键盘导航)".

client/src/components/share-buttons.tsx (1)

49-83: 图标分享控件的无障碍和移动端适配处理良好。

aria-label 已覆盖主要操作,且系统分享按钮按断点显示,交互层次清晰。

As per coding guidelines "无障碍访问(aria 标签、键盘导航)" and "响应式布局是否完整(移动端/平板/桌面端)".

Comment on lines +52 to +54
setAnimating(type);
setTimeout(() => setAnimating(null), 600);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

动画状态有轻微竞态,可能提前结束后一次点击动画。

Line 53 的定时回调会无条件清空 animating;当 600ms 内快速点击不同 reaction 时,前一个回调会把后一个动画提前清掉。

建议修复
-    setAnimating(type);
-    setTimeout(() => setAnimating(null), 600);
+    setAnimating(type);
+    const clickedType = type;
+    setTimeout(() => {
+      setAnimating((prev) => (prev === clickedType ? null : prev));
+    }, 600);
📝 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
setAnimating(type);
setTimeout(() => setAnimating(null), 600);
setAnimating(type);
const clickedType = type;
setTimeout(() => {
setAnimating((prev) => (prev === clickedType ? null : prev));
}, 600);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/post-reactions.tsx` around lines 52 - 54, The animation
state has a race because the unqualified setTimeout callback clears animating
regardless of newer clicks; modify the click handler in the post-reactions
component so you cancel any existing timeout before starting a new one (use a
module-level or component-level ref like timerRef via useRef), then
setAnimating(type) and assign the new timeout id to timerRef so its callback
clears animating only for the most recent timer; also clear the timerRef in the
component cleanup to avoid leaks.

Comment on lines +27 to +31
const handleCopy = () => {
navigator.clipboard.writeText(shareUrl).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}).catch(console.error);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

复制状态的定时器存在并发覆盖,连续点击会导致提示闪烁。

多个 setTimeout 并存时,较早回调会提前把 copied 置回 false。建议复用单个 timer 并在重入时清理。

建议修复
-import { useState } from "react";
+import { useEffect, useRef, useState } from "react";
@@
 export function ShareButtons({ title, url, className = "" }: ShareButtonsProps) {
   const [copied, setCopied] = useState(false);
+  const copyTimerRef = useRef<number | null>(null);
@@
   const handleCopy = () => {
     navigator.clipboard.writeText(shareUrl).then(() => {
       setCopied(true);
-      setTimeout(() => setCopied(false), 2000);
+      if (copyTimerRef.current !== null) window.clearTimeout(copyTimerRef.current);
+      copyTimerRef.current = window.setTimeout(() => setCopied(false), 2000);
     }).catch(console.error);
   };
+
+  useEffect(() => {
+    return () => {
+      if (copyTimerRef.current !== null) window.clearTimeout(copyTimerRef.current);
+    };
+  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/components/share-buttons.tsx` around lines 27 - 31, The copy
handler's setTimeouts can overlap causing flicker; in handleCopy, reuse a single
timer by storing the timeout id in a persistent ref (e.g., copyTimerRef using
useRef<number | null>), call clearTimeout(copyTimerRef.current) before creating
a new setTimeout, assign the new id to copyTimerRef.current, and clear it on
unmount (useEffect cleanup) so setCopied(true) / setCopied(false) cannot be
preempted by earlier timers.

Comment on lines +84 to +89
const toggleSelect = (slug: string) => {
const next = new Set(selectedSlugs);
if (next.has(slug)) next.delete(slug);
else next.add(slug);
setSelectedSlugs(next);
};

Copy link
Copy Markdown

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:

cd client && find . -name "dashboard.tsx" -path "*/pages/admin/*"

Repository: one-ea/Monolith

Length of output: 90


🏁 Script executed:

cat -n client/src/pages/admin/dashboard.tsx | head -150 | tail -80

Repository: one-ea/Monolith

Length of output: 4683


🏁 Script executed:

grep -n "toggleSelect" client/src/pages/admin/dashboard.tsx | head -20

Repository: one-ea/Monolith

Length of output: 527


🏁 Script executed:

cat -n client/src/pages/admin/dashboard.tsx | head -70 | tail -50

Repository: one-ea/Monolith

Length of output: 2347


toggleSelect 应改为函数式状态更新,保持与其他状态更新的一致性

第 85-88 行基于闭包值更新 selectedSlugs,在快速连续点击时可能导致状态变更丢失。此外,同文件第 46 行的 handleDelete 已采用函数式更新模式,建议保持一致。

建议改法
 const toggleSelect = (slug: string) => {
-  const next = new Set(selectedSlugs);
-  if (next.has(slug)) next.delete(slug);
-  else next.add(slug);
-  setSelectedSlugs(next);
+  setSelectedSlugs((prev) => {
+    const next = new Set(prev);
+    if (next.has(slug)) next.delete(slug);
+    else next.add(slug);
+    return next;
+  });
 };
📝 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 toggleSelect = (slug: string) => {
const next = new Set(selectedSlugs);
if (next.has(slug)) next.delete(slug);
else next.add(slug);
setSelectedSlugs(next);
};
const toggleSelect = (slug: string) => {
setSelectedSlugs((prev) => {
const next = new Set(prev);
if (next.has(slug)) next.delete(slug);
else next.add(slug);
return next;
});
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/admin/dashboard.tsx` around lines 84 - 89, The toggleSelect
function currently mutates state using a closed-over selectedSlugs and should
use a functional state update like handleDelete does; change toggleSelect (the
handler that calls setSelectedSlugs) to call setSelectedSlugs(prev => { create a
new Set from prev, add or delete the slug accordingly, and return that new Set
}) so updates are based on the latest state and avoid lost updates on rapid
clicks.

Comment on lines +262 to +264
<button onClick={() => toggleSelect(post.slug)} className={`shrink-0 transition-colors ${selectedSlugs.has(post.slug) ? "text-cyan-400" : "text-muted-foreground/20 group-hover:text-muted-foreground/40"}`}>
{selectedSlugs.has(post.slug) ? <CheckSquare className="h-[14px] w-[14px]" /> : <Square className="h-[14px] w-[14px]" />}
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

为行级选择按钮补齐可访问语义

Line 262 的按钮只有图标,缺少可访问名称和按压状态,读屏用户无法判断“选中哪篇文章/当前是否选中”,会阻断批量操作流程。

可参考的最小修复
- <button onClick={() => toggleSelect(post.slug)} className={`shrink-0 transition-colors ${selectedSlugs.has(post.slug) ? "text-cyan-400" : "text-muted-foreground/20 group-hover:text-muted-foreground/40"}`}>
+ <button
+   type="button"
+   onClick={() => toggleSelect(post.slug)}
+   aria-label={selectedSlugs.has(post.slug) ? `取消选择文章:${post.title}` : `选择文章:${post.title}`}
+   aria-pressed={selectedSlugs.has(post.slug)}
+   className={`shrink-0 transition-colors ${selectedSlugs.has(post.slug) ? "text-cyan-400" : "text-muted-foreground/20 group-hover:text-muted-foreground/40"}`}
+ >
📝 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
<button onClick={() => toggleSelect(post.slug)} className={`shrink-0 transition-colors ${selectedSlugs.has(post.slug) ? "text-cyan-400" : "text-muted-foreground/20 group-hover:text-muted-foreground/40"}`}>
{selectedSlugs.has(post.slug) ? <CheckSquare className="h-[14px] w-[14px]" /> : <Square className="h-[14px] w-[14px]" />}
</button>
<button
type="button"
onClick={() => toggleSelect(post.slug)}
aria-label={selectedSlugs.has(post.slug) ? `取消选择文章:${post.title}` : `选择文章:${post.title}`}
aria-pressed={selectedSlugs.has(post.slug)}
className={`shrink-0 transition-colors ${selectedSlugs.has(post.slug) ? "text-cyan-400" : "text-muted-foreground/20 group-hover:text-muted-foreground/40"}`}
>
{selectedSlugs.has(post.slug) ? <CheckSquare className="h-[14px] w-[14px]" /> : <Square className="h-[14px] w-[14px]" />}
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/pages/admin/dashboard.tsx` around lines 262 - 264, The row-level
select button (toggleSelect, selectedSlugs, post.slug) only renders an icon
(CheckSquare/Square) and lacks accessible semantics; update the button to expose
state and target by adding an aria-pressed attribute bound to
selectedSlugs.has(post.slug) and a clear aria-label (e.g. `${selected ?
'Deselect' : 'Select'} article ${post.title || post.slug}`) so screen readers
know which article and whether it is selected, and keep the existing onClick
calling toggleSelect(post.slug); you can also add a title attribute with the
same text for sighted tooltips.

Comment on lines +349 to +354
{[
{ label: "Sitemap", ok: true },
{ label: "Robots noindex (404)", ok: true },
{ label: "JSON-LD 结构化", ok: true },
{ label: "OG 社交标签", ok: true },
].map(item => (

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

避免把 SEO 固定项写成永久通过

Line 349-354 将 Sitemap/Robots/JSON-LD/OG 全部硬编码为 ok: true,会给后台“始终健康”的假阳性结果,掩盖真实配置问题。建议改为真实探测结果,或明确标注“未检测”。

As per coding guidelines, 页面级组件。审查时请关注: 2. SEO 相关(页面标题、meta 标签)。

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

In `@client/src/pages/admin/dashboard.tsx` around lines 349 - 354, The hardcoded
SEO status array (the anonymous array mapped in the JSX with items like
"Sitemap", "Robots noindex (404)", "JSON-LD 结构化", "OG 社交标签" and property ok:
true) must not always report success; replace the static ok:true with real
detection or an explicit undetected state: implement or call a helper (e.g.,
getSeoStatus or checkSeoStatus) to compute each item's status (or use a
placeholder like status: "unknown"/null while async detection runs) and render
that result instead of the permanent ok:true so the Dashboard component shows
true health only when verified.

@one-ea
one-ea merged commit 45522c7 into main Apr 12, 2026
6 checks passed
one-ea added a commit that referenced this pull request Apr 19, 2026
Critical/High:
- Sanitize custom_header/footer through DOMPurify before injection,
  forbid inline scripts, only allow external script src (fixes #1)
- Add security headers: X-Content-Type-Options, X-Frame-Options,
  Referrer-Policy, Permissions-Policy, HSTS (fixes #5)
- Remove authorEmail from public comment API, use DiceBear avatars
  based on nickname instead of Gravatar hash (fixes #2, #14)
- Add login rate limiting: 5 attempts per 15 min per IP (fixes #3)
- Reflect request Origin in CORS instead of wildcard (fixes #4)
- Add SSRF protection for WebDAV backup and image localization:
  only allow https://, block private/internal IPs (fixes #6, #13)
- Filter javascript: URIs in markdown link renderer (fixes #7)

Medium:
- Replace hardcoded reaction salt with REACTION_SALT env var (fixes #9)
- Add .env.* to .gitignore, remove .env.production from tracking (fixes #10)
- Disable source maps in production build (fixes #11)
- Remove infrastructure details from health endpoint (fixes #15)
one-ea added a commit that referenced this pull request Apr 19, 2026
…se (#38)

* fix(security): harden Monolith against XSS, SSRF, info leaks, and abuse

Critical/High:
- Sanitize custom_header/footer through DOMPurify before injection,
  forbid inline scripts, only allow external script src (fixes #1)
- Add security headers: X-Content-Type-Options, X-Frame-Options,
  Referrer-Policy, Permissions-Policy, HSTS (fixes #5)
- Remove authorEmail from public comment API, use DiceBear avatars
  based on nickname instead of Gravatar hash (fixes #2, #14)
- Add login rate limiting: 5 attempts per 15 min per IP (fixes #3)
- Reflect request Origin in CORS instead of wildcard (fixes #4)
- Add SSRF protection for WebDAV backup and image localization:
  only allow https://, block private/internal IPs (fixes #6, #13)
- Filter javascript: URIs in markdown link renderer (fixes #7)

Medium:
- Replace hardcoded reaction salt with REACTION_SALT env var (fixes #9)
- Add .env.* to .gitignore, remove .env.production from tracking (fixes #10)
- Disable source maps in production build (fixes #11)
- Remove infrastructure details from health endpoint (fixes #15)

* feat(privacy): add cookie consent banner, privacy policy page, and GDPR notice

- Add CookieConsent component: bottom banner with accept/reject,
  stores consent in localStorage, fires 'cookie-consent-accepted'
  event for third-party script injection gating
- Add /privacy route with full privacy policy page (bilingual)
- Gate custom_header/footer script injection behind cookie consent:
  scripts only load after user accepts; non-script HTML injects
  immediately as before
- Add GDPR warning in admin settings 'Extensions & Injection' tab
- Add PRIVACY.md to repo root
- Add privacy page link in cookie consent banner

---------

Co-authored-by: one-ea <one-ea@users.noreply.github.com>
one-ea added a commit that referenced this pull request Apr 21, 2026
…fau5b9au3001u79fbu52a8u7aefu4ee3u7801u5757u4f18u5316u3001u6807u7b7eu6298u53e0 (#40)

* fix(security): harden Monolith against XSS, SSRF, info leaks, and abuse

Critical/High:
- Sanitize custom_header/footer through DOMPurify before injection,
  forbid inline scripts, only allow external script src (fixes #1)
- Add security headers: X-Content-Type-Options, X-Frame-Options,
  Referrer-Policy, Permissions-Policy, HSTS (fixes #5)
- Remove authorEmail from public comment API, use DiceBear avatars
  based on nickname instead of Gravatar hash (fixes #2, #14)
- Add login rate limiting: 5 attempts per 15 min per IP (fixes #3)
- Reflect request Origin in CORS instead of wildcard (fixes #4)
- Add SSRF protection for WebDAV backup and image localization:
  only allow https://, block private/internal IPs (fixes #6, #13)
- Filter javascript: URIs in markdown link renderer (fixes #7)

Medium:
- Replace hardcoded reaction salt with REACTION_SALT env var (fixes #9)
- Add .env.* to .gitignore, remove .env.production from tracking (fixes #10)
- Disable source maps in production build (fixes #11)
- Remove infrastructure details from health endpoint (fixes #15)

* feat(privacy): add cookie consent banner, privacy policy page, and GDPR notice

- Add CookieConsent component: bottom banner with accept/reject,
  stores consent in localStorage, fires 'cookie-consent-accepted'
  event for third-party script injection gating
- Add /privacy route with full privacy policy page (bilingual)
- Gate custom_header/footer script injection behind cookie consent:
  scripts only load after user accepts; non-script HTML injects
  immediately as before
- Add GDPR warning in admin settings 'Extensions & Injection' tab
- Add PRIVACY.md to repo root
- Add privacy page link in cookie consent banner

* fix: 修复移动端代码块横向滚动崩坏问题

- code-block-wrapper 增加 -webkit-overflow-scrolling: touch(iOS 惯性滚动)
- pre 和 code 元素增加 white-space: pre 强制不换行,内容真实撑开 wrapper
- 移动端 pre 增加 width: max-content + min-width: 100% 确保可超出容器宽度
- code-line 从 display: inline-block 改为 display: block,避免 min-width: 100%
  在移动端按父容器宽度错误计算导致代码不撑开横向滚动区域
- 兜底 pre:not(.hljs) 同步修复

* fix: 修复代码块布局坍塌(移除循环宽度依赖)

code-line 改回 display: inline-block,移除 width: max-content 和
min-width: calc(100%+8px) 的循环依赖写法(该写法导致移动端代码
内容完全消失)。pre 的 white-space: pre 已足够阻止换行,
wrapper 的 overflow-x: auto 负责横向滚动。

* feat: 独立页动态导航、后台侧栏固定、移动端代码块优化、标签折叠

- navbar: 从 /api/pages 获取 showInNav=true 的页面动态注入导航栏
- footer: 底部新增独立页面链接
- admin-layout: 侧栏改用 fixed 定位,彻底脱离文档流
- dashboard: 标签区折叠展开,默认显示2行+展开按钮
- globals.css: 移动端代码块优化(行号flex布局、顶栏sticky、尺寸缩减)
- .gitignore: 重写并清理不存在的条目
- 清理 .kilo 和 .playwright-mcp 残留
- 新增 AGENTS.md 和 opencode.json 项目配置

---------

Co-authored-by: one-ea <one-ea@users.noreply.github.com>
one-ea added a commit that referenced this pull request Apr 25, 2026
Critical/High:
- Sanitize custom_header/footer through DOMPurify before injection,
  forbid inline scripts, only allow external script src (fixes #1)
- Add security headers: X-Content-Type-Options, X-Frame-Options,
  Referrer-Policy, Permissions-Policy, HSTS (fixes #5)
- Remove authorEmail from public comment API, use DiceBear avatars
  based on nickname instead of Gravatar hash (fixes #2, #14)
- Add login rate limiting: 5 attempts per 15 min per IP (fixes #3)
- Reflect request Origin in CORS instead of wildcard (fixes #4)
- Add SSRF protection for WebDAV backup and image localization:
  only allow https://, block private/internal IPs (fixes #6, #13)
- Filter javascript: URIs in markdown link renderer (fixes #7)

Medium:
- Replace hardcoded reaction salt with REACTION_SALT env var (fixes #9)
- Add .env.* to .gitignore, remove .env.production from tracking (fixes #10)
- Disable source maps in production build (fixes #11)
- Remove infrastructure details from health endpoint (fixes #15)
one-ea added a commit that referenced this pull request Apr 25, 2026
* fix(security): harden Monolith against XSS, SSRF, info leaks, and abuse

Critical/High:
- Sanitize custom_header/footer through DOMPurify before injection,
  forbid inline scripts, only allow external script src (fixes #1)
- Add security headers: X-Content-Type-Options, X-Frame-Options,
  Referrer-Policy, Permissions-Policy, HSTS (fixes #5)
- Remove authorEmail from public comment API, use DiceBear avatars
  based on nickname instead of Gravatar hash (fixes #2, #14)
- Add login rate limiting: 5 attempts per 15 min per IP (fixes #3)
- Reflect request Origin in CORS instead of wildcard (fixes #4)
- Add SSRF protection for WebDAV backup and image localization:
  only allow https://, block private/internal IPs (fixes #6, #13)
- Filter javascript: URIs in markdown link renderer (fixes #7)

Medium:
- Replace hardcoded reaction salt with REACTION_SALT env var (fixes #9)
- Add .env.* to .gitignore, remove .env.production from tracking (fixes #10)
- Disable source maps in production build (fixes #11)
- Remove infrastructure details from health endpoint (fixes #15)

* chore: add KaTeX math support, clean up project redundancies

- feat: KaTeX math formula rendering ($...$ inline, $$...$$ block)
- fix: protect code blocks from KaTeX parsing
- chore: remove orphaned .wiki-tmp submodule
- chore: remove unused marked import/dep from server
- chore: move @types/dompurify, shadcn to devDependencies
- chore: add .playwright-mcp/, .wiki-tmp/ to .gitignore
- chore: delete stray test artifacts

---------

Co-authored-by: one-ea <one-ea@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend 后端 (Hono Workers) 相关变更 bug Something isn't working feature 新功能 frontend 前端 (React/Vite) 相关变更

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants