# Forge 前端新人操作手册

> 面向**第一次接触 Forge 代码库的前端工程师**，目标是让你在半天内：把项目跑起来、看懂目录结构、掌握本项目的开发约定，并能独立完成"加一个页面 / 加一个组件 / 加一组接口并接上前端"这类常见任务。
>
> 本手册所有代码片段与文件路径均取自当前代码库，可直接对照查阅。

**目录**

- [1. Forge 是什么](#1-forge-是什么)
- [2. 环境准备与首次启动](#2-环境准备与首次启动)
- [3. 项目结构总览](#3-项目结构总览)
- [4. 必须先懂的 5 个概念](#4-必须先懂的-5-个概念)
- [5. 样式与设计系统](#5-样式与设计系统)
- [6. UI 组件库（components/ui）](#6-ui-组件库componentsui)
- [7. 业务组件分层](#7-业务组件分层)
- [8. 数据获取：Hook 模式](#8-数据获取hook-模式)
- [9. API 路由](#9-api-路由)
- [10. 状态管理与跨组件同步](#10-状态管理与跨组件同步)
- [11. 国际化（i18n）](#11-国际化i18n)
- [12. 图标](#12-图标)
- [13. 常见任务 Cookbook](#13-常见任务-cookbook)
- [14. 开发命令一览](#14-开发命令一览)
- [15. 调试与常见坑](#15-调试与常见坑)
- [16. 代码规范约定](#16-代码规范约定)
- [17. 进阶参考文档](#17-进阶参考文档)

---

## 1. Forge 是什么

**Forge = 跑在你电脑上的 AI 工作站**，基于 Claude Agent SDK 构建，核心能力是：桌面聊天、IM 远程操控（飞书/Telegram/Discord）、可视化定时任务、可复用的 Marketplace 方案模板。

它是一个 **Electron 桌面应用 + Web 应用双端**的 Next.js 工程，同一套前端代码既被打包进 Electron 桌面壳，也能以独立 Web 服务部署。

### 技术栈一览

| 维度 | 选型 |
|---|---|
| 框架 | Next.js 15（App Router + Turbopack） |
| UI 库 | React 19 |
| 语言 | TypeScript（`strict: true`） |
| 样式 | Tailwind CSS 3 + CSS 变量设计令牌 |
| 桌面壳 | Electron 40 |
| 包管理 | **pnpm**（必须用 pnpm，不要用 npm/yarn） |
| 数据库 | better-sqlite3（本地 SQLite） |
| 认证 | better-auth（session cookie） |
| 图标 | lucide-react |
| 代码编辑器 | CodeMirror 6（`@uiw/react-codemirror`） |
| Markdown | react-markdown + remark-gfm + shiki 高亮 |
| 表单校验 | zod |
| 测试 | vitest |

> 前端新人重点关注 **App Router、Tailwind、自定义 Hook、components/ui** 这几块即可；Electron 主进程、IM Bridge、Agent SDK 这些后端/集成层不在前端日常改动范围内，了解存在即可。

---

## 2. 环境准备与首次启动

### 2.1 前置依赖

| 依赖 | 最低版本 | 说明 |
|---|---|---|
| Node.js | 20+ | 推荐 LTS |
| pnpm | 9+ | `npm install -g pnpm` |
| Claude Code CLI | 已安装并登录 | **仅当你需要本地跑通 Agent 功能时必需**；纯前端 UI 开发可不装 |

### 2.2 安装

```bash
git clone <repo-url> forge
cd forge
pnpm install      # better-sqlite3 等会触发本地编译，首次稍慢，属正常
```

### 2.3 环境变量

复制模板并按需填写：

```bash
cp .env.example .env     # Next.js 自动加载 .env
```

前端开发最常打交道的是这几个（详见 `.env.example`）：

- `AUTH_MODE`：`zh`（国内：手机号+验证码）/ `en`（国际：邮箱+Google+GitHub）。它会被 `next.config.ts` 注入为 `NEXT_PUBLIC_AUTH_MODE` 供前端读取。
- `FORGE_DATA_DIR`：本地数据目录，默认 `~/.forge`。
- 各类功能开关，命名统一 `FORGE_` 前缀（如 `FORGE_FEISHU_USER_AUTH`）。

### 2.4 启动命令（选一个）

| 命令 | 作用 | 适用场景 |
|---|---|---|
| `pnpm dev:next` | 只起 Next.js（Turbopack），浏览器访问 `localhost:3000` | **纯前端开发首选，启动最快** |
| `pnpm dev:web` | 同上，但监听 `0.0.0.0`（局域网可访问） | 在手机/其它设备上调试布局 |
| `pnpm dev` | 同时起 Next.js + Electron 桌面壳 | 需要验证桌面端集成时 |

> 💡 **90% 的前端日常用 `pnpm dev:next` 就够了**，改代码热更新秒级生效，不用等 Electron。

### 2.5 首次启动后会发生什么

1. 打开 `http://localhost:3000`，未登录会被中间件重定向到 `/login`（详见 [§4.4](#44鉴权middleware--session)）。
2. 完成登录（根据 `AUTH_MODE` 走对应流程）。
3. 进入主应用 `AppLayout`，选择/打开一个项目文件夹（workspace）。
4. 之后即可进入对话、IM、定时任务等视图。

---

## 3. 项目结构总览

```
forge/
├── src/
│   ├── app/                 # ① Next.js App Router：页面 + API 路由
│   │   ├── layout.tsx       #    根布局（字体 + 全局 Provider）
│   │   ├── page.tsx         #    首页（鉴权 → 渲染 AppLayout）
│   │   ├── globals.css      #    全局样式 + 设计令牌（CSS 变量）
│   │   ├── login/           #    登录页
│   │   ├── invite/[token]/  #    邀请接受页
│   │   └── api/             #    后端 API 路由（每个子目录一组接口）
│   ├── components/
│   │   ├── ui/              # ② 基础组件库（Button/Input/Card...）
│   │   ├── layout/          # ③ 应用骨架（侧边栏/标题栏/AppLayout）
│   │   ├── views/           # ④ 顶层业务视图（chat/im/settings...）
│   │   ├── manage/          #    管理 panels（agents/skills/mcp）
│   │   ├── chat/            #    聊天相关组件
│   │   ├── marketplace/     #    模板市场组件
│   │   └── providers/       # ⑤ 全局 Provider（Theme/I18n）
│   ├── hooks/               # ⑥ 自定义 Hooks（数据获取的主要载体）
│   ├── lib/                 # ⑦ 业务逻辑/工具/SDK 客户端（cn、auth、db、i18n...）
│   ├── styles/              # ⑧ 设计系统文档（COLOR_TOKENS.md / Z_INDEX.md）
│   └── types/               #    共享 TS 类型
├── electron/                # Electron 主进程/预加载（前端一般不碰）
├── tailwind.config.ts       # Tailwind 主题（语义色映射到 CSS 变量）
├── middleware.ts            # 路由级鉴权守卫
├── forge-design.md          # ★ 设计系统规范（强烈推荐先读）
└── docs/                    # 项目文档（本手册就在此）
```

**一句话记忆**：`app/` 管路由与接口，`components/` 管界面，`hooks/` 管数据，`lib/` 管工具与逻辑。

---

## 4. 必须先懂的 5 个概念

### 4.1 App Router 约定

- 文件即路由：`src/app/login/page.tsx` → `/login`；`src/app/api/workspaces/route.ts` → `GET/POST /api/workspaces`。
- 动态段用方括号：`invite/[token]/page.tsx`、`api/workspaces/[id]/route.ts`。
- 页面默认是 **Server Component**；API 路由导出 `GET/POST/PATCH/DELETE` 等命名函数。

### 4.2 Server vs Client Component

| | Server Component（默认） | Client Component |
|---|---|---|
| 标志 | 无 | 文件顶部 `'use client'` |
| 能用 | `async`、直接查 DB/调 SDK | `useState`/`useEffect`/事件/`fetch`/浏览器 API |
| 用于 | 页面骨架、鉴权重定向 | 几乎所有交互组件、Hooks、`components/ui` |

**项目实践**：页面（`page.tsx`）常是 Server Component 负责鉴权，再把真正的内容交给 Client Component。典型如首页：

```tsx
// src/app/page.tsx —— Server Component
import { redirect } from 'next/navigation'
import { AppLayout } from '@/components/layout/app-layout'
import { getCurrentUser } from '@/lib/auth/session'

export default async function Home() {
  const user = await getCurrentUser()
  if (!user) redirect('/login?returnTo=%2F')   // 服务端鉴权 + 重定向
  return <AppLayout />                          // 交给客户端组件渲染主体
}
```

> 判断要不要加 `'use client'` 的简单规则：**用到任何 hook 或浏览器 API/事件，就必须加。**

### 4.3 路径别名 `@/`

`tsconfig.json` 配置了 `"@/*": ["./src/*"]`，全项目统一用 `@/` 导入：

```ts
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import type { Workspace } from '@/lib/types'
```

**不要写相对路径**（如 `../../lib/utils`），一律用 `@/`。

### 4.4 鉴权（middleware + session）

两层鉴权，前端都要知道：

1. **路由守卫 `middleware.ts`**：对除 `/login`、`/api/auth`、`/api/webhooks`、`/api/upload` 外的所有路径，检查 `better-auth.session_token` cookie。
   - 无 cookie 访问页面 → 重定向 `/login?returnTo=...`。
   - 无 cookie 访问 `/api/*` → 直接返回 `401`。
   - 因此**前端 `fetch('/api/...')` 一定要处理 401**（见 [§8](#8-数据获取hook-模式) 与 `src/lib/auth/client-redirect.ts`）。

2. **接口内校验**：API 路由内部用 `requireUser()`（必校验，失败抛错）或 `getCurrentUser()`（返回 `null` 由调用方判断）。

### 4.5 数据流向（单向，不要绕过）

```
组件 ──调用──▶ Hook（fetch）──HTTP──▶ app/api/*/route.ts ──▶ SQLite(better-sqlite3)
组件 ◀──状态── Hook ◀──JSON─── app/api/*/route.ts ◀──读取─── SQLite
```

- 组件**不直接** `fetch`，统一通过 Hook。
- 前端类型 ≠ 数据库字段：Hook 内做 `snake_case → camelCase` 映射（见 [§8](#8-数据获取hook-模式)）。

---

## 5. 样式与设计系统

### 5.1 用语义色，不要写裸十六进制

颜色全部走 **CSS 变量 + Tailwind 别名**，定义在 `src/app/globals.css`，映射在 `tailwind.config.ts`。这样暗色/亮色自动切换。

| 用途 | 推荐类名 | 说明 |
|---|---|---|
| 页面底色 | `bg-page` | 最底层背景 |
| 表面色阶 | `bg-surface-1` … `bg-surface-4` | 层级递进，卡片/悬浮/激活 |
| 悬浮/激活 | `hover:bg-surface-3` `active:bg-surface-4` | 交互态 |
| 文本 | `text-primary` / `text-secondary` / `text-tertiary` | 主/次/弱 |
| 品牌/强调 | `bg-brand-primary` `text-on-primary` `text-accent-danger` | 主按钮、成功/警告/危险 |

> ❌ 不要写 `bg-[#0B0B0E]` `text-white` 这类硬编码值；✅ 用上面这些语义类。

### 5.2 暗色 / 亮色

- **默认深色**（`:root`）；浅色通过 `<html data-theme="light">` 触发。
- 切换由 `ThemeProvider` 管理，组件里用 `useTheme()` 读取/设置，**不要手动操作 `data-theme`**。

### 5.3 `cn()` 合并类名

`src/lib/utils.ts` 提供唯一的类名合并工具：

```ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
```

**任何接受 `className` 的组件，都要用 `cn()` 把外部传入的 className 与内部默认类合并**，让外部可覆盖：

```tsx
<div className={cn('p-4 rounded-lg bg-surface-2', className)} />
```

### 5.4 字体

根布局注入了三个 CSS 变量字体（`src/app/layout.tsx`）：`--font-body`（正文）、`--font-mono`（代码）、`--font-heading`（标题）。用 Tailwind 别名：`font-body` / `font-mono` / `font-heading`。

### 5.5 必读设计文档（写 UI 前先扫一眼）

- `forge-design.md`（根目录）：完整设计规范，组件尺寸/间距/变体的权威来源。
- `src/styles/COLOR_TOKENS.md`：颜色令牌清单。
- `src/styles/Z_INDEX.md`：层级（z-index）约定——**涉及弹层/抽屉/遮罩时务必查它**，别乱写 `z-50`。

---

## 6. UI 组件库（components/ui）

### 6.1 现有组件清单

| 交互控件 | 展示/预览类 |
|---|---|
| `button` `input` `switch` `tabs` `custom-select` `badge` `card` `empty-state` `resize-handle` | `markdown-preview` `code-editor` `codemirror-editor` `json-preview` `html-preview` `svg-preview` `pdf-preview` `audio-preview` `video-preview` `binary-preview` |

> 目录里还有 `index.ts`（barrel 导出）。导入时按需从具体文件导入即可：`import { Button } from '@/components/ui/button'`。

### 6.2 组件编写约定（严格遵守）

参照 `src/components/ui/button.tsx`，一个规范组件长这样：

```tsx
'use client'

import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react'
import { cn } from '@/lib/utils'
import { Loader2 } from 'lucide-react'

export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'
export type ButtonSize = 'sm' | 'md' | 'lg'

export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: ButtonVariant
  size?: ButtonSize
  loading?: boolean
  leftIcon?: ReactNode
  rightIcon?: ReactNode
  fullWidth?: boolean
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
  { variant = 'secondary', size = 'md', className, children, ...rest },
  ref,
) {
  return (
    <button ref={ref} className={cn('base-classes', variant, size, className)} {...rest}>
      {children}
    </button>
  )
})
```

**约定清单**：

1. **`forwardRef`** 包裹（便于父组件拿 DOM 引用）。
2. **命名导出**（`export const Button`），**不要默认导出**。
3. Props **继承原生 HTML 属性**（`extends ButtonHTMLAttributes<...>`），并 `...rest` 透传。
4. 类型用 `interface` 并 `export`，方便复用。
5. 类名一律走 `cn()`，外部 `className` 放最后以允许覆盖。
6. 文件名 **kebab-case**（`custom-select.tsx`），组件名 **PascalCase**（`CustomSelect`）。

### 6.3 用法示例

```tsx
import { Button } from '@/components/ui/button'
import { Loader2, Save } from 'lucide-react'

<Button variant="primary" size="md" loading={saving} leftIcon={<Save className="w-4 h-4" />} onClick={handleSave}>
  保存
</Button>
```

- `variant`：`primary`（主 CTA，品牌色）/ `secondary`（默认，带边框）/ `ghost`（极简）/ `danger`（危险操作）。
- `size`：`sm`=32px / `md`=36px（默认）/ `lg`=40px。
- `loading` 自带转圈图标并禁用点击。

---

## 7. 业务组件分层

| 目录 | 职责 | 何时放这里 |
|---|---|---|
| `components/ui` | 通用、无业务含义的基础控件 | 可在任意项目复用的原子组件 |
| `components/layout` | 应用骨架：`app-layout` / `left-sidebar` / `right-sidebar` / `title-bar` / `user-menu` / `project-modal` | 框架级布局与导航 |
| `components/views` | 顶层业务视图：`chat-view` / `im-view` / `settings-view` / `schedule-view` / `marketplace-view` / `team-view` / `manage-view` | 一整屏业务功能 |
| `components/manage` | 管理面板：agents / skills / mcp 的 panel 与 editor | 视图内部的"管理"子功能 |
| `components/chat` | 聊天专属（agent-block、slash-command-menu） | 仅对话相关的细分组件 |
| `components/marketplace` | 模板市场（template-list/editor） | 仅市场相关 |
| `components/providers` | 全局 Provider：`theme-provider` / `i18n-provider` | 跨页面的上下文 |

**放置原则**：从"是否业务专用"判断——通用进 `ui`，业务进对应目录；拿不准就参照最相近的现有组件归位。

---

## 8. 数据获取：Hook 模式

### 8.1 核心规则

- **不用 SWR / React Query**，统一用原生 `fetch` + `useState` + `useEffect`。
- 每个后端资源对应一个 `src/hooks/use-<resource>.ts`，导出一个聚合了 **数据 + loading + 增删改方法** 的 hook。
- 组件只调 hook，不直接 fetch。

### 8.2 完整范例（逐段讲解）

以 `src/hooks/use-workspaces.ts` 为模板：

```tsx
'use client'
import { useState, useEffect, useCallback } from 'react'
import type { Workspace } from '@/lib/types'
import { readArrayResponse } from '@/lib/auth/client-redirect'

// 1) 定义"数据库行"类型（snake_case，和表结构一致）
interface DbWorkspace {
  id: string; path: string; name: string
  last_opened_at: string; created_at: string; exists?: boolean
}

// 2) 映射函数：DB 行 → 前端类型（camelCase）
function mapWorkspace(row: DbWorkspace): Workspace {
  return { id: row.id, path: row.path, name: row.name,
           lastOpenedAt: row.last_opened_at, createdAt: row.created_at, exists: row.exists }
}

export function useWorkspaces() {
  const [workspaces, setWorkspaces] = useState<Workspace[]>([])
  const [loading, setLoading] = useState(true)

  // 3) 读取（首次 + 可复用的 refresh）
  const fetchWorkspaces = useCallback(async () => {
    try {
      const res = await fetch('/api/workspaces')
      const data = await readArrayResponse<DbWorkspace>(res)   // 统一处理 401/数组
      setWorkspaces(data.map(mapWorkspace))
    } catch (err) {
      console.error('Failed to fetch workspaces:', err)
    } finally {
      setLoading(false)
    }
  }, [])
  useEffect(() => { fetchWorkspaces() }, [fetchWorkspaces])

  // 4) 写操作：调 API 成功后，本地"乐观更新"状态
  const removeProject = useCallback(async (id: string) => {
    const res = await fetch(`/api/workspaces/${id}`, { method: 'DELETE' })
    if (!res.ok) throw new Error(`Failed to remove project: ${res.status}`)
    setWorkspaces((prev) => prev.filter((w) => w.id !== id))
  }, [])

  // 5) 把数据、状态、动作一起返回
  return { workspaces, loading, openProjectFolder, removeProject, refreshWorkspaces: fetchWorkspaces }
}
```

**要点提炼**：

- ✅ `readArrayResponse`（来自 `src/lib/auth/client-redirect.ts`）：统一读取数组型响应并处理鉴权跳转，**数组接口用它**而非裸 `res.json()`。
- ✅ 写操作成功后**本地更新状态**（乐观更新），而不是整表重新拉取——保持响应迅速。
- ✅ `loading` 初始为 `true`，请求结束（无论成败）置 `false`。
- ✅ 错误 `console.error` 记录；面向用户的提示由调用方组件决定。

### 8.3 现有 Hooks 速查

`use-workspaces` `use-sessions` `use-chat` `use-agents` `use-agent-config` `use-skills` `use-slash-commands` `use-mcp-servers` `use-models` `use-api-providers` `use-settings` `use-marketplace` `use-cron-tasks` `use-task-executions` `use-im-channels` `use-invitations` `use-webhook-tokens` `use-folder-dialog` `use-container-width` `use-word-wrap`。

> 新增资源时，**先看有没有现成 hook**；同名资源若已存在，扩展它而不是新建。

---

## 9. API 路由

### 9.1 组织方式

- 位于 `src/app/api/<resource>/route.ts`，按 REST 风格。
- 单项操作放 `[id]/route.ts`，例如 `api/workspaces/route.ts`（列表/创建）+ `api/workspaces/[id]/route.ts`（取/改/删）。

### 9.2 写法模板

```ts
// src/app/api/<resource>/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { requireUser } from '@/lib/auth/session'
import { getDb } from '@/lib/db'

export async function GET() {
  await requireUser()                       // 鉴权：未登录直接抛错
  const rows = getDb().prepare('SELECT * FROM resource').all()
  return NextResponse.json(rows)
}

export async function POST(req: NextRequest) {
  const user = await requireUser()
  const body = await req.json()
  // ...写入 DB
  return NextResponse.json({ ok: true, id })
}
```

- 鉴权二选一：`requireUser()`（必校验，返回 user）/ `getCurrentUser()`（返回 `null` 自行判断）。
- 返回统一用 `NextResponse.json(...)`，可带 `status`。

### 9.3 流式接口（SSE）

`/api/chat` 和 `/api/im-events` 用 **Server-Sent Events** 流式推送。前端用 `ReadableStream` reader 逐块读取（见 `use-chat.ts`、`use-im-channels.ts` 的处理方式）。改造这类接口前，先读这两个 hook 理解既有协议。

---

## 10. 状态管理与跨组件同步

**没有 Redux/Zustand**。状态分三层：

1. **组件本地**：`useState`。
2. **跨组件上下文**：`components/providers` 下的 Provider + 对应 `useXxx`。
   - `ThemeProvider` → `useTheme()`：`{ theme, resolvedTheme, setTheme }`。
   - `I18nProvider` → `useI18n()`：`{ locale, setLocale, t }`。
3. **跨实例/跨 hook 同步**：**自定义 DOM 事件**。当某处数据变化需要通知其它已挂载的 hook 时，广播事件：

```ts
// 发送方：写完设置后广播
window.dispatchEvent(new CustomEvent('forge:settings-changed', { detail: updates }))

// 接收方：在 hook 里监听
useEffect(() => {
  const handler = (e: Event) => {
    const detail = (e as CustomEvent).detail
    if (detail) setSettings(prev => ({ ...prev, ...detail }))
  }
  window.addEventListener('forge:settings-changed', handler)
  return () => window.removeEventListener('forge:settings-changed', handler)
}, [])
```

> 事件名约定 `forge:<resource>-changed`（如 `forge:settings-changed`、`forge:sessions-changed`、`forge:files-changed`）。新增同步需求时沿用此命名。**不要为此引入状态管理库。**

---

## 11. 国际化（i18n）

- 支持 `zh` / `en`，翻译表在 `src/lib/i18n.ts`（数百条）。
- 用法：

```tsx
import { useI18n } from '@/components/providers/i18n-provider'

function MyComponent() {
  const { t, locale, setLocale } = useI18n()
  return <h1>{t('settings.title')}</h1>   // zh→"设置" / en→"Settings"
}
```

- **新增界面文案一律走 `t('key')`，不要硬编码中文字符串。** key 用命名空间风格（`settings.title`、`chat.newSession`）。
- 根布局默认 `lang="zh"`；切换语言时 Provider 会自动同步 `document.documentElement.lang`。

---

## 12. 图标

统一用 `lucide-react`，按需命名导入：

```tsx
import { Save, Loader2, ChevronRight } from 'lucide-react'

<Save className="w-4 h-4" />           // 常规：w-4 h-4 (16px) 或 w-5 h-5 (20px)
<Loader2 className="w-4 h-4 animate-spin" />  // loading 转圈
```

**约定**：尺寸用 `w-4 h-4` / `w-5 h-5`；加载态用 `Loader2 + animate-spin`。不要引入其它图标库。

---

## 13. 常见任务 Cookbook

### 任务 1：加一个新页面

1. 在 `src/app/<route>/` 下建 `page.tsx`。
2. 需要鉴权就仿首页：`const user = await getCurrentUser(); if (!user) redirect('/login?returnTo=...')`。
3. 需要交互则在同目录或 `components/` 建一个 `'use client'` 组件，页面引用它。
4. 参照 `src/app/login/page.tsx`。

> 中间件会自动保护该路由（未登录重定向），无需手动加守卫。

### 任务 2：加一个 UI 基础组件

1. 在 `src/components/ui/<name>.tsx` 新建文件。
2. 套用 [§6.2](#62-组件编写约定严格遵守) 模板：`'use client'` + `forwardRef` + `cn()` + 命名导出 + Props 继承原生属性。
3. 类名用语义色（`bg-surface-2` 等），尺寸遵循 `forge-design.md`。
4. 参照 `button.tsx` / `input.tsx`。

### 任务 3：加一组「API + Hook + 前端调用」完整链路（最常用）

假设要做一个"标签（tags）"资源的 CRUD：

1. **建表/类型**：在 `src/lib/db.ts` 加迁移；在 `src/lib/types.ts` 加 `Tag` 接口（camelCase）。
2. **API**：
   - `src/app/api/tags/route.ts` → 导出 `GET`（列表）、`POST`（新建）。
   - `src/app/api/tags/[id]/route.ts` → 导出 `PATCH`、`DELETE`。
   - 内部 `await requireUser()` 鉴权，返回 `NextResponse.json`。
3. **Hook**：复制 `use-workspaces.ts` 改造为 `src/hooks/use-tags.ts`——定义 `DbTag`，写 `mapTag`，`fetch` + `useState/useEffect` + 增删改乐观更新。
4. **组件**：在视图里 `const { tags, loading, createTag, deleteTag } = useTags()`，渲染列表、绑定按钮。

**自检**：组件里有没有直接 `fetch`？有则挪进 hook；hook 有没有做 snake→camel 映射？列表接口有没有用 `readArrayResponse`？

### 任务 4：在已有 hook 上扩展动作

需求："给 use-agents 加一个'复制 agent'"。**不要新建 hook**，在 `use-agents.ts` 内加 `duplicateAgent(id)`：`fetch POST` → 成功后 `setAgents(prev => [...prev, mapped])` → 返回新对象。保持该资源所有操作集中在一个 hook。

### 任务 5：加图标

直接 `import { IconName } from 'lucide-react'`，用 `className` 控尺寸/颜色。在 [lucide.dev](https://lucide.dev) 搜图标名。

---

## 14. 开发命令一览

| 命令 | 作用 |
|---|---|
| `pnpm dev:next` | **前端日常开发**：仅 Next.js + Turbopack，`localhost:3000` |
| `pnpm dev:web` | 同上，监听 `0.0.0.0`（局域网可访问） |
| `pnpm dev` | Next.js + Electron 桌面壳同时启动 |
| `pnpm build:next` | 生产构建（Next.js） |
| `pnpm build` | 构建前端 + Electron（打包前置） |
| `pnpm build:server` | 构建独立 Web 服务产物（`dist-server`） |
| `pnpm start:web` | 以生产模式启动 Next.js |
| `pnpm start:server` | 运行构建好的 server（`node dist-server/server.js`） |
| `pnpm typecheck` | **提交前必跑**：`tsc --noEmit` 全量类型检查 |
| `pnpm test` | vitest 单测 |
| `pnpm package:mac` / `:win` / `:linux` | 打对应平台桌面安装包 |

---

## 15. 调试与常见坑

- **改了代码没生效？** 确认是 `pnpm dev:next` 在跑；偶发缓存问题删 `.next/` 重启。
- **页面一直跳登录？** 检查 session cookie 是否存在；`AUTH_MODE` 与登录方式是否匹配；`middleware.ts` 的 `PUBLIC_PATHS` 是否覆盖了你的新公开路由。
- **接口 401？** 多半 session 过期；`readArrayResponse` 已统一处理跳转，自定义 fetch 记得也处理 401。
- **类型报错一堆？** 先 `pnpm typecheck` 看全量；注意 `strict: true`，避免 `any`，DB 行与前端类型用映射函数转换。
- **样式不生效/层级不对？** 查 `src/styles/Z_INDEX.md`；确认用的是语义色而非硬编码；暗色下颜色异常多半是误用了非语义值。
- **SSE 接口调试：** 看 `use-chat.ts` 如何解析流；`next.config.ts` 里 `compress: false` 就是为了不缓冲 SSE。
- **Tailwind 自定义类没提示：** 内容扫描配置在 `tailwind.config.ts` 的 `content: ['./src/**/*.{ts,tsx}']`，新文件在 `src/` 下即自动纳入。

---

## 16. 代码规范约定

| 项 | 约定 |
|---|---|
| 文件名 | kebab-case：`use-im-channels.ts`、`agent-block.tsx` |
| 组件名 | PascalCase：`AgentBlock` |
| Hook 名 | camelCase 且以 `use` 开头：`useImChannels` |
| 导出 | **命名导出**为主，UI 组件/Hook 都不要默认导出 |
| 导入路径 | 一律 `@/` 别名，禁用深层相对路径 |
| 类型 | 严格模式，Props 用 `interface` 并导出；DB↔前端用映射函数 |
| 样式 | Tailwind 语义类 + `cn()`，禁内联 style 写颜色 |
| 文案 | 走 `t('key')`，不硬编码中文 |
| 图标 | 仅 `lucide-react` |
| 状态 | 原生 hook + Provider + 自定义事件，不引第三方状态库 |
| 提交前 | 至少跑 `pnpm typecheck` |

---

## 17. 进阶参考文档

| 文档 | 内容 |
|---|---|
| `README_CN.md` | 项目整体介绍、功能、安装、配置 |
| `forge-design.md` | 设计系统规范（颜色/字号/间距/组件尺寸的权威定义） |
| `src/styles/COLOR_TOKENS.md` | 颜色令牌清单 |
| `src/styles/Z_INDEX.md` | 层级（z-index）约定 |
| `docs/server-deployment.md` | Web 服务部署 |
| `docs/forge-architecture.html` | 架构图 |

---

> **上手建议**：先把项目用 `pnpm dev:next` 跑起来 → 通读 `forge-design.md` → 用 `use-workspaces.ts` + `button.tsx` 两个文件理解数据流与组件约定 → 挑一个 [任务 3](#任务-3加一组api--hook--前端调用完整链路最常用) 的小资源练手。欢迎提问。🎉
