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 | 包管理 + 脚本运行 |
常用命令
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.md、component-registry.md。
样式
- 只用语义 token:
bg-kumo-base/bg-kumo-elevated/bg-kumo-canvas、text-kumo-default/text-kumo-subtle、border-kumo-line/border-kumo-hairline。 - 禁止原生 Tailwind 颜色:
bg-blue-500、text-gray-900会破坏主题(例外:bg-white/bg-black/text-white/text-black/transparent)。 - 禁止
dark:变体:明暗模式由 CSSlight-dark()自动适配。在根元素用data-mode="light" | "dark"控制;三态切换(浅色/深色/跟随系统)见src/lib/use-theme.ts+ 头像下拉「外观」。 - 合并 className 用
cn()(从@cloudflare/kumo导出):cn("base", cond && "extra", className)。
样式接入(已配置好,勿改顺序)
src/app/globals.css 顶部三行顺序固定:
@source "../../node_modules/@cloudflare/kumo/dist/**/*.{js,jsx,ts,tsx}"; /* 让 Tailwind 扫描 kumo 内部类名 */
@import "@cloudflare/kumo/styles"; /* 必须先于 tailwindcss,注册 @theme token */
@import "tailwindcss";
组件导入
// 主包导入
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>等。 Button的icon传组件引用:<Button icon={PlusIcon}>。Surface已废弃 → 用LayerCard(无variantprop,直接<LayerCard className="p-6">)。Text的size只对body/secondary/success/error有效;heading*不允许size,mono*只允许lg。Input没有iconprop;图标写在按钮/菜单里(Button icon={...}、DropdownMenu.Item icon={...})。Table.Row/Cell/CheckCell继承原生tr/td属性(支持onClick/className);行选中态用内置<Table.Row variant="selected">。Sidebar.MenuButton支持href+active,可直接当导航链接用。
Sidebar 的两个坑(本项目已规避)
- Hydration mismatch:
Sidebar用useState(() => window.matchMedia(...).matches)初始化isMobile,SSR 判定为桌面、客户端首帧按实际宽度判定,视口 < 默认断点 768px 时 DOM 不一致 → hydration 错误(Recoverable Error)。规避:<Sidebar.Provider mobileBreakpoint={1}>(仅当不需要移动端抽屉时)。 - 意外折叠:
collapsible默认是"icon",误触Sidebar.Trigger会把侧栏收成 57px 只剩图标。本项目要固定布局,已设collapsible="none"并移除了顶栏的Sidebar.Trigger。
Next.js 16 注意事项
⚠️ 这是新版 Next.js,API/约定可能与你训练数据不同。 写代码前请参考 node_modules/next/dist/docs/ 下的官方指南,并留意弃用提示。
- 动态路由的
params是 Promise,需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 已加 overrides 把 react/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"] 作为兜底。
目录结构
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」→/app。proxy.ts粗查 cookie,app/app/layout.tsx经getCurrentUser验签名 + 查sessions(未过期)+ 联查users做权威守卫。退出 =destroySession删sessions那一行 + 清 cookie,会话立即失效(可强制吊销)。登录只走真实微信扫码,无开发绕过。 - 列目录:page(RSC)→
getActiveProjectId(user.id)(cookie + 归属校验)→listActiveProjectFolder→getStorageForProject解密凭证 → S3ListObjectsV2(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_id;active_projectcookie 每次读都重校验归属。
环境变量(见 .env.example)
DATABASE_URL(Supabase Postgres,注意该端点暂不支持 SSL,PGSSL=false)、AUTH_SECRET(会话签名)、ENCRYPTION_KEY(凭证加密主密钥)、WECHAT_LOGIN_*(微信登录 5 个)。
参考
- Kumo 官方 agent 指南(monorepo 视角,仅供了解库本身):https://raw.githubusercontent.com/cloudflare/kumo/refs/heads/main/AGENTS.md
- Kumo 组件 registry:
node_modules/@cloudflare/kumo/ai/component-registry.json - 微信登录接入:
.claude/skills/wechat-login-integration/(state 算法 / 回调 / token 契约以此为准)