账户菜单

Drive — AI 协作指南

基于 Next.js 16 (App Router, Turbopack) + React 19 + Tailwind CSS v4 + Cloudflare Kumo UI多租户 SaaS 网盘:微信扫码登录,用户建多个"项目"绑定阿里云 OSS / 腾讯云 COS / S3 兼容存储,网盘实时代理对象存储列表。使用 Bun 作为包管理器。

技术栈

工具 版本 说明
Next.js 16.2.10 App Router,默认 Turbopack
React 19.2.4 RSC + Client Components
Tailwind CSS 4.3.3 v4,CSS-first 配置
@cloudflare/kumo 2.8.0 Cloudflare 组件库(Base UI + Tailwind v4),ESM-only
@phosphor-icons/react 2.1.10 Kumo 配套图标
pg (node-postgres) 8.22 Supabase Postgres 客户端 + 自管 SQL 迁移
@aws-sdk/client-s3 3.1090 统一对接 OSS / COS / R2(S3 兼容端点)
Bun 1.2.x 包管理 + 脚本运行

常用命令

Bash
bun run dev      # 启动 dev server (http://localhost:3000)
bun run build    # 生产构建
bun run start    # 启动生产服务
bun run lint     # ESLint
bun run typecheck  # 类型检查(tsc --noEmit)
bun run db:migrate # 执行数据库迁移(migrations/*.sql,幂等)

Kumo 使用规范(重要)

组件 API 权威来源:node_modules/@cloudflare/kumo/ai/component-registry.json 使用某个组件前先查它(jq 或阅读),不要凭记忆猜 props。 另有易读版:node_modules/@cloudflare/kumo/ai/USAGE.mdcomponent-registry.md

样式

  • 只用语义 tokenbg-kumo-base / bg-kumo-elevated / bg-kumo-canvastext-kumo-default / text-kumo-subtleborder-kumo-line / border-kumo-hairline
  • 禁止原生 Tailwind 颜色bg-blue-500text-gray-900 会破坏主题(例外:bg-white/bg-black/text-white/text-black/transparent)。
  • 禁止 dark: 变体:明暗模式由 CSS light-dark() 自动适配。在根元素用 data-mode="light" | "dark" 控制;三态切换(浅色/深色/跟随系统)见 src/lib/use-theme.ts + 头像下拉「外观」。
  • 合并 className 用 cn()(从 @cloudflare/kumo 导出):cn("base", cond && "extra", className)

样式接入(已配置好,勿改顺序)

src/app/globals.css 顶部三行顺序固定:

CSS
@source "../../node_modules/@cloudflare/kumo/dist/**/*.{js,jsx,ts,tsx}"; /* 让 Tailwind 扫描 kumo 内部类名 */
@import "@cloudflare/kumo/styles";   /* 必须先于 tailwindcss,注册 @theme token */
@import "tailwindcss";

组件导入

TSX
// 主包导入
import { Button, Input, LayerCard, Text, Badge } from "@cloudflare/kumo";
// 细粒度导入(更好的 tree-shaking)
import { Button } from "@cloudflare/kumo/components/button";
// 图标
import { PlusIcon, GearIcon } from "@phosphor-icons/react";
  • 许多组件是复合组件,用点语法:<Dialog.Root><Dialog.Trigger><Select.Option> 等。
  • Buttonicon 传组件引用:<Button icon={PlusIcon}>
  • Surface 已废弃 → 用 LayerCard(无 variant prop,直接 <LayerCard className="p-6">)。
  • Textsize 只对 body/secondary/success/error 有效;heading* 不允许 sizemono* 只允许 lg
  • Input 没有 icon prop;图标写在按钮/菜单里(Button icon={...}DropdownMenu.Item icon={...})。
  • Table.Row/Cell/CheckCell 继承原生 tr/td 属性(支持 onClick/className);行选中态用内置 <Table.Row variant="selected">
  • Sidebar.MenuButton 支持 href + active,可直接当导航链接用。
  1. Hydration mismatchSidebaruseState(() => window.matchMedia(...).matches) 初始化 isMobile,SSR 判定为桌面、客户端首帧按实际宽度判定,视口 < 默认断点 768px 时 DOM 不一致 → hydration 错误(Recoverable Error)。规避<Sidebar.Provider mobileBreakpoint={1}>(仅当不需要移动端抽屉时)。
  2. 意外折叠collapsible 默认是 "icon",误触 Sidebar.Trigger 会把侧栏收成 57px 只剩图标。本项目要固定布局,已设 collapsible="none" 并移除了顶栏的 Sidebar.Trigger

Next.js 16 注意事项

⚠️ 这是新版 Next.js,API/约定可能与你训练数据不同。 写代码前请参考 node_modules/next/dist/docs/ 下的官方指南,并留意弃用提示。

  • 动态路由的 paramsPromise,需 await(如 const { path } = await params)。
  • cookies() / headers() / searchParams 同样是 Promise,需 await
  • middleware 已改名为 proxy:文件是 src/proxy.ts,导出函数必须叫 proxy(或 default),不能叫 middleware,否则报 "must export a function"。matcher 用 config.matcher
  • 可选 catch-all [[...path]] 与同级 page.tsx 冲突:匹配 /app 的只能是其中一个。根目录列表由 [[...path]] 内部处理,不要再建 app/app/page.tsx
  • 具体静态路由(如 /app/recent)优先级高于 catch-all,可共存。

依赖去重(react 类型冲突)

kumo / @phosphor-icons/react 若被 bun 装成各自的嵌套副本,会出现 Type 'Icon' is not assignable to type 'ReactNode | Icon'(react 类型双份)。package.json 已加 overridesreact/react-dom/@phosphor-icons/react/@types/react 收敛到根版本;改依赖后若复发,删掉 node_modules bun.lock 重装。

Kumo / 图标 与 RSC 的坑

@cloudflare/kumo@phosphor-icons/react客户端组件库。在 Server Component 里直接渲染会因 React 服务端环境缺少 createContext 而报错((0, d.createContext) is not a function)。

做法:使用这些组件的页面/组件顶部加 "use client"(本项目网盘 UI 集中在 src/components/drive/*,均为 client)。next.config.ts 已配置 transpilePackages: ["@cloudflare/kumo", "@phosphor-icons/react"] 作为兜底。

目录结构

Plain Text
src/
├── proxy.ts               # /app 路由守卫(旧称 middleware,Edge 仅查 cookie 存在性)
├── app/
│   ├── globals.css          # Tailwind v4 + Kumo 样式接入
│   ├── layout.tsx           # 根布局(Geist 字体,bg-kumo-canvas)
│   ├── page.tsx             # 首页 → redirect("/app")
│   ├── login/page.tsx       # 微信扫码登录页
│   ├── auth/wechat/callback/route.ts  # 微信登录回调(验 state→换用户→建会话)
│   ├── app/
│   │   ├── layout.tsx       # /app 段布局(async 权威守卫:getCurrentUser,未登录→/login)
│   │   ├── [[...path]]/page.tsx  # 我的文件 + 嵌套目录(catch-all,真实 S3 列表)
│   │   ├── recent|starred|trash/page.tsx  # 侧边栏视图(v1 仍走 mock)
│   │   └── settings/        # projects/page.tsx 存储项目管理;profile/page.tsx 配置文件(个人资料)
│   └── api/
│       ├── auth/            # wechat/state、me、logout
│       ├── projects/        # 项目 CRUD + [id]/activate(设当前项目)
│       └── drive/           # list(列目录)+ download(预签名下载)
├── components/
│   ├── auth/wechat-login-panel.tsx  # 微信二维码面板(加载平台 SDK)
│   ├── drive/
│   │   ├── app-shell.tsx    # Sidebar.Provider + Sidebar + 顶栏(核心布局)
│   │   ├── drive-view.tsx   # 两栏目录视图(列表 + 详情面板,activeKey 状态)
│   │   ├── file-table.tsx   # 文件列表(Table,多选 + 单选高亮 + 行操作菜单)
│   │   ├── file-details.tsx # 右侧详情面板(选中条目的基本信息 + 下载/复制链接)
│   │   ├── user-menu.tsx    # 顶栏用户菜单(微信头像/昵称 + 配置文件/外观/退出)
│   │   ├── file-icon.tsx    # 按类型渲染文件图标
│   │   └── empty-state.tsx  # 空目录占位
│   └── projects/
│       ├── projects-panel.tsx       # 项目列表(provider Badge + 编辑/删除 + 设为当前)
│       ├── project-form-dialog.tsx  # 新建/编辑项目 Dialog(Select provider + SensitiveInput)
│       └── project-switcher.tsx     # 侧栏当前项目切换 Select
└── lib/
    ├── api.ts               # requireUser / handleApiError / ValidationError(路由层鉴权+错误映射)
    ├── use-theme.ts         # 三态主题 hook(light/dark/system,localStorage + 根元素 data-mode;首屏由 layout 内联脚本定调)
    ├── crypto 见 storage/crypto.ts
    ├── db/
    │   ├── index.ts         # pg Pool 单例(globalThis 缓存防热重载)+ query()
    │   └── migrate.ts       # 迁移执行器(schema_migrations 记录版本,幂等)
    ├── auth/
    │   ├── session.ts       # 服务端会话:createSession/destroySession + getCurrentUser()(sessions 表,cookie 仅存签名 session id)
    │   └── wechat.ts        # 微信 state 签发/校验 + code 换用户 + 归并本地用户
    ├── projects/
    │   ├── repo.ts          # 项目 DB 访问(唯一"加密写"点,带 owner_id 隔离)
    │   ├── active.ts        # 当前项目 cookie(active_project,每次读重校验归属)
    │   ├── validate.ts      # 项目表单校验
    │   └── dto.ts           # 前端项目视图类型(脱敏)
    ├── storage/
    │   ├── types.ts         # StorageProvider 接口 + ProjectConfig
    │   ├── crypto.ts        # AES-256-GCM encrypt/decrypt(ENCRYPTION_KEY)
    │   ├── s3.ts            # createS3Provider(@aws-sdk/client-s3,OSS/COS/R2 通用)
    │   └── index.ts         # getStorageForProject(唯一"解密读"点)
    └── drive/
        ├── types.ts         # DriveItem / FolderListing 等模型
        ├── crumbs.ts        # 面包屑构造(mock 与真实列表共用)
        ├── listing.ts       # listActiveProjectFolder(解密→S3→FolderListing)
        ├── mock.ts          # 静态假数据(仅 recent/starred/trash 用)
        └── utils.ts         # cn / formatBytes / formatDate / kindFromName

运行时数据流(关键)

  • 登录/login 扫微信码 → /auth/wechat/callback 验 state、code 换用户、按 unionid>openid>sub 归并 → createSession(uid)sessions 表插一行(固定 7 天)→ cookie 只存「HMAC 签名的 session id」→ /appproxy.ts 粗查 cookie,app/app/layout.tsxgetCurrentUser 验签名 + 查 sessions(未过期)+ 联查 users 做权威守卫。退出 = destroySessionsessions 那一行 + 清 cookie,会话立即失效(可强制吊销)。登录只走真实微信扫码,无开发绕过。
  • 列目录:page(RSC)→ getActiveProjectId(user.id)(cookie + 归属校验)→ listActiveProjectFoldergetStorageForProject 解密凭证 → S3 ListObjectsV2(Delimiter:"/")FolderListing 渲染。无项目→引导创建 EmptyState;连不上→友好报错 EmptyState。
  • 凭证:项目 AK/SK 一律 AES-256-GCM 加密落 projects.access_key_id_enc/secret_access_key_enc,仅 storage/index.ts 服务端解密,绝不下发浏览器;API 只回 maskProject 脱敏视图。
  • 多租户:所有 projects 查询带 owner_idactive_project cookie 每次读都重校验归属。

环境变量(见 .env.example)

DATABASE_URL(Supabase Postgres,注意该端点暂不支持 SSL,PGSSL=false)、AUTH_SECRET(会话签名)、ENCRYPTION_KEY(凭证加密主密钥)、WECHAT_LOGIN_*(微信登录 5 个)。

参考

发布于 2026/7/31 16:51:45

生成