better-chatbot

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

better-chatbot Contribution & Standards Skill

better-chatbot 贡献与标准技能

Status: Production Ready Last Updated: 2025-11-04 (v2.1.0 - Added extension points + UX patterns) Dependencies: None (references better-chatbot project) Latest Versions: Next.js 15.3.2, Vercel AI SDK 5.0.82, Better Auth 1.3.34, Drizzle ORM 0.41.0

状态:生产就绪 最后更新:2025-11-04(v2.1.0 - 新增扩展点 + UX模式) 依赖:无(引用better-chatbot项目) 最新版本:Next.js 15.3.2、Vercel AI SDK 5.0.82、Better Auth 1.3.34、Drizzle ORM 0.41.0

Overview

概述

better-chatbot is an open-source AI chatbot platform for individuals and teams, built with Next.js 15 and Vercel AI SDK v5. It combines multi-model AI support (OpenAI, Anthropic, Google, xAI, Ollama, OpenRouter) with advanced features like MCP (Model Context Protocol) tool integration, visual workflow builder, realtime voice assistant, and team collaboration.
This skill teaches Claude the project-specific conventions and patterns used in better-chatbot to ensure contributions follow established standards and avoid common pitfalls.

better-chatbot 是一款面向个人和团队的开源AI聊天机器人平台,基于Next.js 15和Vercel AI SDK v5构建。它结合了多模型AI支持(OpenAI、Anthropic、Google、xAI、Ollama、OpenRouter)与高级功能,如MCP(Model Context Protocol)工具集成、可视化工作流构建器、实时语音助手及团队协作功能。
本技能向Claude传授better-chatbot项目的专属规范与模式,确保贡献内容符合既定标准,避免常见问题。

Project Architecture

项目架构

Directory Structure

目录结构

better-chatbot/
├── src/
│   ├── app/                    # Next.js App Router + API routes
│   │   ├── api/[resource]/     # RESTful API organized by domain
│   │   ├── (auth)/             # Auth route group
│   │   ├── (chat)/             # Chat UI route group
│   │   └── store/              # Zustand stores
│   ├── components/             # UI components by domain
│   │   ├── layouts/
│   │   ├── agent/
│   │   ├── chat/
│   │   └── export/
│   ├── lib/                    # Core logic and utilities
│   │   ├── action-utils.ts     # Server action validators (CRITICAL)
│   │   ├── ai/                 # AI integration (models, tools, MCP, speech)
│   │   ├── db/                 # Database (Drizzle ORM + repositories)
│   │   ├── validations/        # Zod schemas
│   │   └── [domain]/           # Domain-specific helpers
│   ├── hooks/                  # Custom React hooks
│   │   ├── queries/            # Data fetching hooks
│   │   └── use-*.ts
│   └── types/                  # TypeScript types by domain
├── tests/                      # E2E tests (Playwright)
├── docs/                       # Setup guides and tips
├── docker/                     # Docker configs
└── drizzle/                    # Database migrations

better-chatbot/
├── src/
│   ├── app/                    # Next.js App Router + API路由
│   │   ├── api/[resource]/     # 按领域组织的RESTful API
│   │   ├── (auth)/             # 认证路由组
│   │   ├── (chat)/             # 聊天UI路由组
│   │   └── store/              # Zustand状态存储
│   ├── components/             # 按领域划分的UI组件
│   │   ├── layouts/
│   │   ├── agent/
│   │   ├── chat/
│   │   └── export/
│   ├── lib/                    # 核心逻辑与工具库
│   │   ├── action-utils.ts     # 服务器操作验证器(关键)
│   │   ├── ai/                 # AI集成(模型、工具、MCP、语音)
│   │   ├── db/                 # 数据库(Drizzle ORM + 仓库)
│   │   ├── validations/        # Zod校验规则
│   │   └── [domain]/           # 领域专属助手
│   ├── hooks/                  # 自定义React钩子
│   │   ├── queries/            # 数据获取钩子
│   │   └── use-*.ts
│   └── types/                  # 按领域划分的TypeScript类型
├── tests/                      # E2E测试(Playwright)
├── docs/                       # 安装指南与技巧
├── docker/                     # Docker配置
└── drizzle/                    # 数据库迁移

API Architecture & Design Patterns

API架构与设计模式

Route Structure Philosophy

路由结构理念

Convention: RESTful resources with Next.js App Router conventions
/api/[resource]/route.ts         → GET/POST collection endpoints
/api/[resource]/[id]/route.ts    → GET/PUT/DELETE item endpoints
/api/[resource]/actions.ts       → Server actions (mutations)
规范:遵循Next.js App Router约定的RESTful资源
/api/[resource]/route.ts         → GET/POST 集合端点
/api/[resource]/[id]/route.ts    → GET/PUT/DELETE 单项端点
/api/[resource]/actions.ts       → 服务器操作(变更)

Standard Route Handler Pattern

标准路由处理器模式

Location:
src/app/api/
Template structure:
typescript
export async function POST(request: Request) {
  try {
    // 1. Parse and validate request body with Zod
    const json = await request.json();
    const parsed = zodSchema.parse(json);

    // 2. Check authentication
    const session = await getSession();
    if (!session?.user.id) return new Response("Unauthorized", { status: 401 });

    // 3. Check authorization (ownership/permissions)
    if (resource.userId !== session.user.id) return new Response("Forbidden", { status: 403 });

    // 4. Load/compose dependencies (tools, context, etc.)
    const tools = await loadMcpTools({ mentions, allowedMcpServers });

    // 5. Execute with streaming if applicable
    const stream = createUIMessageStream({ execute: async ({ writer }) => { ... } });

    // 6. Return response
    return createUIMessageStreamResponse({ stream });
  } catch (error) {
    logger.error(error);
    return Response.json({ message: error.message }, { status: 500 });
  }
}
位置
src/app/api/
模板结构:
typescript
export async function POST(request: Request) {
  try {
    // 1. 使用Zod解析并验证请求体
    const json = await request.json();
    const parsed = zodSchema.parse(json);

    // 2. 检查认证状态
    const session = await getSession();
    if (!session?.user.id) return new Response("Unauthorized", { status: 401 });

    // 3. 检查授权(所有权/权限)
    if (resource.userId !== session.user.id) return new Response("Forbidden", { status: 403 });

    // 4. 加载/组合依赖(工具、上下文等)
    const tools = await loadMcpTools({ mentions, allowedMcpServers });

    // 5. 若适用则以流式处理执行
    const stream = createUIMessageStream({ execute: async ({ writer }) => { ... } });

    // 6. 返回响应
    return createUIMessageStreamResponse({ stream });
  } catch (error) {
    logger.error(error);
    return Response.json({ message: error.message }, { status: 500 });
  }
}

Shared Business Logic Pattern

共享业务逻辑模式

Key Insight: Extract complex orchestration logic into shared utilities
Example:
src/app/api/chat/shared.chat.ts
This file demonstrates how to handle:
  • Tool loading (
    loadMcpTools
    ,
    loadWorkFlowTools
    ,
    loadAppDefaultTools
    )
  • Filtering and composition (
    filterMCPToolsByMentions
    ,
    excludeToolExecution
    )
  • System prompt building (
    mergeSystemPrompt
    )
  • Manual tool execution handling
Pattern:
typescript
// Shared utility function
export const loadMcpTools = (opt?) =>
  safe(() => mcpClientsManager.tools())
    .map((tools) => {
      if (opt?.mentions?.length) {
        return filterMCPToolsByMentions(tools, opt.mentions);
      }
      return filterMCPToolsByAllowedMCPServers(tools, opt?.allowedMcpServers);
    })
    .orElse({} as Record<string, VercelAIMcpTool>);

// Used in multiple routes
// - /api/chat/route.ts
// - /api/chat/temporary/route.ts
// - /api/workflow/[id]/execute/route.ts
Why: DRY principle, single source of truth, consistent behavior
核心洞察:将复杂的编排逻辑提取到共享工具库中
示例
src/app/api/chat/shared.chat.ts
该文件展示了如何处理:
  • 工具加载(
    loadMcpTools
    loadWorkFlowTools
    loadAppDefaultTools
  • 过滤与组合(
    filterMCPToolsByMentions
    excludeToolExecution
  • 系统提示构建(
    mergeSystemPrompt
  • 手动工具执行处理
模式:
typescript
// 共享工具函数
export const loadMcpTools = (opt?) =>
  safe(() => mcpClientsManager.tools())
    .map((tools) => {
      if (opt?.mentions?.length) {
        return filterMCPToolsByMentions(tools, opt.mentions);
      }
      return filterMCPToolsByAllowedMCPServers(tools, opt?.allowedMcpServers);
    })
    .orElse({} as Record<string, VercelAIMcpTool>);

// 在多个路由中使用
// - /api/chat/route.ts
// - /api/chat/temporary/route.ts
// - /api/workflow/[id]/execute/route.ts
优势:遵循DRY原则,单一可信来源,行为一致

Defensive Programming with safe()

基于safe()的防御式编程

Library:
ts-safe
for functional error handling
Philosophy: Never crash the chat - degrade features gracefully
typescript
// Returns empty object on failure, chat continues
const MCP_TOOLS = await safe()
  .map(errorIf(() => !isToolCallAllowed && "Not allowed"))
  .map(() => loadMcpTools({ mentions, allowedMcpServers }))
  .orElse({});  // Graceful fallback
:使用
ts-safe
进行函数式错误处理
理念:绝不导致聊天崩溃 - 优雅降级功能
typescript
// 失败时返回空对象,聊天继续运行
const MCP_TOOLS = await safe()
  .map(errorIf(() => !isToolCallAllowed && "Not allowed"))
  .map(() => loadMcpTools({ mentions, allowedMcpServers }))
  .orElse({});  // 优雅回退

Streaming-First Architecture

优先流式处理架构

Pattern: Use Vercel AI SDK streaming utilities
typescript
// In route handler
const stream = createUIMessageStream({
  execute: async ({ writer }) => {
    // Stream intermediate results
    writer.write({ type: "text", content: "Processing..." });

    // Execute with streaming
    const result = await streamText({
      model,
      messages,
      tools,
      onChunk: (chunk) => writer.write({ type: "text-delta", delta: chunk })
    });

    return { output: result };
  }
});

return createUIMessageStreamResponse({ stream });
Why: Live feedback, better UX, handles long-running operations

模式:使用Vercel AI SDK流式工具
typescript
// 在路由处理器中
const stream = createUIMessageStream({
  execute: async ({ writer }) => {
    // 流式传输中间结果
    writer.write({ type: "text", content: "Processing..." });

    // 以流式方式执行
    const result = await streamText({
      model,
      messages,
      tools,
      onChunk: (chunk) => writer.write({ type: "text-delta", delta: chunk })
    });

    return { output: result };
  }
});

return createUIMessageStreamResponse({ stream });
优势:实时反馈,更好的用户体验,处理长时间运行的操作

Tool System Deep Dive

工具系统深度解析

Three-Tier Tool Architecture

三层工具架构

Design Goal: Balance extensibility (MCP), composability (workflows), and batteries-included (default tools)
Tier 1: MCP Tools (External)
  ↓ Can be used in
Tier 2: Workflow Tools (User-Created)
  ↓ Can be used in
Tier 3: Default Tools (Built-In)
设计目标:平衡扩展性(MCP)、组合性(工作流)和开箱即用性(默认工具)
第一层:MCP工具(外部)
  ↓ 可用于
第二层:工作流工具(用户创建)
  ↓ 可用于
第三层:默认工具(内置)

Tier 1: MCP Tools (External Integrations)

第一层:MCP工具(外部集成)

Location:
src/lib/ai/mcp/
Philosophy: Model Context Protocol servers become first-class tools
Manager Pattern:
typescript
// mcp-manager.ts - Singleton for all MCP clients
export const mcpClientsManager = globalThis.__mcpClientsManager__;

// API:
mcpClientsManager.init()              // Initialize configured servers
mcpClientsManager.getClients()        // Get connected clients
mcpClientsManager.tools()             // Get all tools as Vercel AI SDK tools
mcpClientsManager.toolCall(serverId, toolName, args)  // Execute tool
Why Global Singleton?
  • Next.js dev hot-reloading → reconnecting MCP servers on every change is expensive
  • Persists across HMR updates
  • Production: only one instance needed
Tool Wrapping:
typescript
// MCP tools are tagged with metadata for filtering
type VercelAIMcpTool = Tool & {
  _mcpServerId: string;
  _originToolName: string;
  _toolName: string; // Transformed for AI SDK
};

// Branded type for runtime checking
VercelAIMcpToolTag.create(tool)
位置
src/lib/ai/mcp/
理念:Model Context Protocol服务器成为一等工具
管理器模式:
typescript
// mcp-manager.ts - 所有MCP客户端的单例
export const mcpClientsManager = globalThis.__mcpClientsManager__;

// API:
mcpClientsManager.init()              // 初始化已配置的服务器
mcpClientsManager.getClients()        // 获取已连接的客户端
mcpClientsManager.tools()             // 获取所有Vercel AI SDK格式的工具
mcpClientsManager.toolCall(serverId, toolName, args)  // 执行工具
为何使用全局单例?
  • Next.js开发热重载 → 每次变更都重新连接MCP服务器成本高昂
  • 在HMR更新之间保持持久化
  • 生产环境:仅需一个实例
工具包装:
typescript
// MCP工具标记元数据用于过滤
type VercelAIMcpTool = Tool & {
  _mcpServerId: string;
  _originToolName: string;
  _toolName: string; // 转换为AI SDK格式
};

// 用于运行时检查的品牌类型
VercelAIMcpToolTag.create(tool)

Tier 2: Workflow Tools (Visual Composition)

第二层:工作流工具(可视化组合)

Location:
src/lib/ai/workflow/
Philosophy: Visual workflows become callable tools via
@workflow_name
Node Types:
typescript
enum NodeKind {
  Input = "input",      // Entry point
  LLM = "llm",          // AI reasoning
  Tool = "tool",        // Call MCP/default tools
  Http = "http",        // HTTP requests
  Template = "template",// Text processing
  Condition = "condition", // Branching logic
  Output = "output",    // Exit point
}
Execution with Streaming:
typescript
// Workflows stream intermediate results
executor.subscribe((e) => {
  if (e.eventType == "NODE_START") {
    dataStream.write({
      type: "tool-output-available",
      toolCallId,
      output: { status: "running", node: e.nodeId }
    });
  }
  if (e.eventType == "NODE_END") {
    dataStream.write({
      type: "tool-output-available",
      toolCallId,
      output: { status: "complete", result: e.result }
    });
  }
});
Key Feature: Live progress updates in chat UI
位置
src/lib/ai/workflow/
理念:可视化工作流通过
@workflow_name
成为可调用工具
节点类型:
typescript
enum NodeKind {
  Input = "input",      // 入口点
  LLM = "llm",          // AI推理
  Tool = "tool",        // 调用MCP/默认工具
  Http = "http",        // HTTP请求
  Template = "template",// 文本处理
  Condition = "condition", // 分支逻辑
  Output = "output",    // 出口点
}
流式执行:
typescript
// 工作流流式传输中间结果
executor.subscribe((e) => {
  if (e.eventType == "NODE_START") {
    dataStream.write({
      type: "tool-output-available",
      toolCallId,
      output: { status: "running", node: e.nodeId }
    });
  }
  if (e.eventType == "NODE_END") {
    dataStream.write({
      type: "tool-output-available",
      toolCallId,
      output: { status: "complete", result: e.result }
    });
  }
});
核心功能:聊天UI中的实时进度更新

Tier 3: Default Tools (Built-In Capabilities)

第三层:默认工具(内置能力)

Location:
src/lib/ai/tools/
Categories:
typescript
export const APP_DEFAULT_TOOL_KIT = {
  [AppDefaultToolkit.Visualization]: {
    CreatePieChart, CreateBarChart, CreateLineChart,
    CreateTable, CreateTimeline
  },
  [AppDefaultToolkit.WebSearch]: {
    WebSearch, WebContent
  },
  [AppDefaultToolkit.Http]: {
    Http
  },
  [AppDefaultToolkit.Code]: {
    JavascriptExecution, PythonExecution
  },
};
Tool Implementation Pattern:
typescript
// Execution returns "Success", rendering happens client-side
export const createTableTool = createTool({
  description: "Create an interactive table...",
  inputSchema: z.object({
    title: z.string(),
    columns: z.array(...),
    data: z.array(...)
  }),
  execute: async () => "Success"
});

// Client-side rendering in components/tool-invocation/
export function InteractiveTable({ part }) {
  const args = part.input;
  return <DataTable columns={args.columns} data={args.data} />;
}
Why Separation?
  • Server: Pure data/business logic
  • Client: Rich visualization/interaction
  • Easier testing, better performance
位置
src/lib/ai/tools/
分类:
typescript
export const APP_DEFAULT_TOOL_KIT = {
  [AppDefaultToolkit.Visualization]: {
    CreatePieChart, CreateBarChart, CreateLineChart,
    CreateTable, CreateTimeline
  },
  [AppDefaultToolkit.WebSearch]: {
    WebSearch, WebContent
  },
  [AppDefaultToolkit.Http]: {
    Http
  },
  [AppDefaultToolkit.Code]: {
    JavascriptExecution, PythonExecution
  },
};
工具实现模式:
typescript
// 执行返回"Success",渲染在客户端完成
export const createTableTool = createTool({
  description: "Create an interactive table...",
  inputSchema: z.object({
    title: z.string(),
    columns: z.array(...),
    data: z.array(...)
  }),
  execute: async () => "Success"
});

// 客户端渲染位于components/tool-invocation/
export function InteractiveTable({ part }) {
  const args = part.input;
  return <DataTable columns={args.columns} data={args.data} />;
}
为何分离?
  • 服务器:纯数据/业务逻辑
  • 客户端:丰富的可视化/交互
  • 更易测试,性能更佳

Tool Lifecycle

工具生命周期

1. Request → /api/chat/route.ts
2. Parse mentions (@tool, @workflow, @agent)
3. Load tools based on mentions/permissions:
   - loadMcpTools() → filters by mentions or allowedMcpServers
   - loadWorkFlowTools() → converts workflows to tools
   - loadAppDefaultTools() → filters default toolkits
4. Merge all tools into single Record<string, Tool>
5. Handle toolChoice mode:
   - "manual" → LLM proposes, user confirms
   - "auto" → full execution
   - "none" → no tools loaded
6. Pass tools to streamText()
7. Stream results back
1. 请求 → /api/chat/route.ts
2. 解析提及内容(@tool、@workflow、@agent)
3. 根据提及内容/权限加载工具:
   - loadMcpTools() → 按提及内容或允许的MCP服务器过滤
   - loadWorkFlowTools() → 将工作流转换为工具
   - loadAppDefaultTools() → 过滤默认工具包
4. 将所有工具合并为单个Record<string, Tool>
5. 处理toolChoice模式:
   - "manual" → LLM提议,用户确认
   - "auto" → 全自动执行
   - "none" → 不加载工具
6. 将工具传入streamText()
7. 流式返回结果

Convention-Based Extension

基于规范的扩展

Adding a new tool type is simple:
  1. Add enum to
    AppDefaultToolkit
  2. Implement tool with
    createTool()
  3. Add to
    APP_DEFAULT_TOOL_KIT
  4. Tool automatically available via
    @toolname

添加新工具类型很简单:
  1. AppDefaultToolkit
    添加枚举
  2. 使用
    createTool()
    实现工具
  3. 添加到
    APP_DEFAULT_TOOL_KIT
  4. 工具自动通过
    @toolname
    可用

Component & Design Philosophy

组件与设计理念

Organization by Feature

按功能组织

Location:
src/components/
components/
├── ui/              → shadcn/ui primitives
├── layouts/         → App structure
├── agent/           → Agent-specific
├── workflow/        → Workflow editor
├── tool-invocation/ → Tool result rendering
└── *.tsx            → Shared components
Principle: Group by feature, not by type
位置
src/components/
components/
├── ui/              → shadcn/ui 基础组件
├── layouts/         → 应用结构
├── agent/           → Agent专属组件
├── workflow/        → 工作流编辑器
├── tool-invocation/ → 工具结果渲染
└── *.tsx            → 共享组件
原则:按功能分组,而非按类型

Compound Component Pattern

复合组件模式

Example:
message.tsx
+
message-parts.tsx
Philosophy: Break complex components into composable parts
typescript
// message.tsx exports multiple related components
export function PreviewMessage({ message }) { ... }
export function ErrorMessage({ error }) { ... }

// message-parts.tsx handles polymorphic content
export function MessageParts({ parts }) {
  return parts.map(part => {
    if (isToolUIPart(part)) return <ToolInvocation part={part} />;
    if (part.type === 'text') return <Markdown text={part.text} />;
    // ... other types
  });
}
示例
message.tsx
+
message-parts.tsx
理念:将复杂组件拆分为可组合的部分
typescript
// message.tsx导出多个相关组件
export function PreviewMessage({ message }) { ... }
export function ErrorMessage({ error }) { ... }

// message-parts.tsx处理多态内容
export function MessageParts({ parts }) {
  return parts.map(part => {
    if (isToolUIPart(part)) return <ToolInvocation part={part} />;
    if (part.type === 'text') return <Markdown text={part.text} />;
    // ... 其他类型
  });
}

Client Component Wrapper Pattern

客户端组件包装模式

Example:
chat-bot.tsx
Structure:
typescript
export default function ChatBot({ threadId, initialMessages }) {
  // 1. State management (Zustand)
  const [model, toolChoice] = appStore(useShallow(state => [...]));

  // 2. Vercel AI SDK hook
  const { messages, append, status } = useChat({
    id: threadId,
    initialMessages,
    body: { chatModel: model, toolChoice },
  });

  // 3. Render orchestration
  return (
    <>
      <ChatGreeting />
      <MessageList messages={messages} />
      <PromptInput onSubmit={append} />
    </>
  );
}
Why: Top-level orchestrates, delegates rendering to specialized components
示例
chat-bot.tsx
结构:
typescript
export default function ChatBot({ threadId, initialMessages }) {
  // 1. 状态管理(Zustand)
  const [model, toolChoice] = appStore(useShallow(state => [...]));

  // 2. Vercel AI SDK钩子
  const { messages, append, status } = useChat({
    id: threadId,
    initialMessages,
    body: { chatModel: model, toolChoice },
  });

  // 3. 渲染编排
  return (
    <>
      <ChatGreeting />
      <MessageList messages={messages} />
      <PromptInput onSubmit={append} />
    </>
  );
}
优势:顶层负责编排,将渲染委托给专用组件

Tool Result Rendering Separation

工具结果渲染分离

Key Architecture Decision:
  • Tool execution lives in
    lib/ai/tools/
  • Tool rendering lives in
    components/tool-invocation/
Example:
typescript
// Server-side (lib/ai/tools/create-table.ts)
execute: async (params) => "Success"

// Client-side (components/tool-invocation/interactive-table.tsx)
export function InteractiveTable({ part }) {
  const { columns, data } = part.input;
  return <DataTable columns={columns} data={data} />;
}
Benefits:
  • Clear separation of concerns
  • Easier testing
  • Client can be rich/interactive without server complexity

关键架构决策:
  • 工具执行位于
    lib/ai/tools/
  • 工具渲染位于
    components/tool-invocation/
示例:
typescript
// 服务器端(lib/ai/tools/create-table.ts)
execute: async (params) => "Success"

// 客户端(components/tool-invocation/interactive-table.tsx)
export function InteractiveTable({ part }) {
  const { columns, data } = part.input;
  return <DataTable columns={columns} data={data} />;
}
收益:
  • 清晰的关注点分离
  • 更易测试
  • 客户端可实现丰富交互,无需服务器复杂逻辑

Database & Repository Patterns

数据库与仓库模式

Repository Pattern Architecture

仓库模式架构

Location:
src/lib/db/
Structure:
db/
├── repository.ts          → Single import point
├── pg/
│   ├── db.pg.ts          → Drizzle connection
│   ├── schema.pg.ts      → Table definitions
│   └── repositories/     → Feature queries
└── migrations/           → Drizzle migrations
Philosophy: Abstract DB behind repository interfaces
位置
src/lib/db/
结构:
db/
├── repository.ts          → 单一导入入口
├── pg/
│   ├── db.pg.ts          → Drizzle连接
│   ├── schema.pg.ts      → 表定义
│   └── repositories/     → 功能查询
└── migrations/           → Drizzle迁移
理念:通过仓库接口抽象数据库

Interface-First Design

接口优先设计

Pattern:
typescript
// 1. Define interface in src/types/[domain].ts
export type ChatRepository = {
  insertThread(thread: Omit<ChatThread, "createdAt">): Promise<ChatThread>;
  selectThread(id: string): Promise<ChatThread | null>;
  selectThreadDetails(id: string): Promise<ThreadDetails | null>;
};

// 2. Implement in src/lib/db/pg/repositories/[domain]-repository.pg.ts
export const pgChatRepository: ChatRepository = {
  selectThreadDetails: async (id: string) => {
    const [thread] = await db
      .select()
      .from(ChatThreadTable)
      .leftJoin(UserTable, eq(ChatThreadTable.userId, UserTable.id))
      .where(eq(ChatThreadTable.id, id));

    if (!thread) return null;

    const messages = await pgChatRepository.selectMessagesByThreadId(id);

    return {
      id: thread.chat_thread.id,
      title: thread.chat_thread.title,
      userId: thread.chat_thread.userId,
      createdAt: thread.chat_thread.createdAt,
      userPreferences: thread.user?.preferences,
      messages,
    };
  },
};

// 3. Export from src/lib/db/repository.ts
export const chatRepository = pgChatRepository;
Why:
  • Easy to swap implementations (pg → sqlite)
  • Testable without database
  • Consistent API across codebase
模式:
typescript
// 1. 在src/types/[domain].ts中定义接口
export type ChatRepository = {
  insertThread(thread: Omit<ChatThread, "createdAt">): Promise<ChatThread>;
  selectThread(id: string): Promise<ChatThread | null>;
  selectThreadDetails(id: string): Promise<ThreadDetails | null>;
};

// 2. 在src/lib/db/pg/repositories/[domain]-repository.pg.ts中实现
export const pgChatRepository: ChatRepository = {
  selectThreadDetails: async (id: string) => {
    const [thread] = await db
      .select()
      .from(ChatThreadTable)
      .leftJoin(UserTable, eq(ChatThreadTable.userId, UserTable.id))
      .where(eq(ChatThreadTable.id, id));

    if (!thread) return null;

    const messages = await pgChatRepository.selectMessagesByThreadId(id);

    return {
      id: thread.chat_thread.id,
      title: thread.chat_thread.title,
      userId: thread.chat_thread.userId,
      createdAt: thread.chat_thread.createdAt,
      userPreferences: thread.user?.preferences,
      messages,
    };
  },
};

// 3. 从src/lib/db/repository.ts导出
export const chatRepository = pgChatRepository;
优势:
  • 轻松切换实现(pg → sqlite)
  • 无需数据库即可测试
  • 整个代码库的API一致

Query Optimization Strategies

查询优化策略

1. Indexes on Foreign Keys:
typescript
export const ChatThreadTable = pgTable("chat_thread", {
  id: uuid("id").primaryKey(),
  userId: uuid("user_id").references(() => UserTable.id),
}, (table) => ({
  userIdIdx: index("chat_thread_user_id_idx").on(table.userId),
}));
2. Selective Loading:
typescript
// Load minimal data
selectThread(id){ id, title, userId, createdAt }

// Load full data when needed
selectThreadDetails(id){ ...thread, messages, userPreferences }
3. SQL Aggregation:
typescript
// Get threads with last message timestamp
const threadsWithActivity = await db
  .select({
    threadId: ChatThreadTable.id,
    lastMessageAt: sql<string>`MAX(${ChatMessageTable.createdAt})`,
  })
  .from(ChatThreadTable)
  .leftJoin(ChatMessageTable, eq(ChatThreadTable.id, ChatMessageTable.threadId))
  .groupBy(ChatThreadTable.id)
  .orderBy(desc(sql`last_message_at`));
1. 外键索引:
typescript
export const ChatThreadTable = pgTable("chat_thread", {
  id: uuid("id").primaryKey(),
  userId: uuid("user_id").references(() => UserTable.id),
}, (table) => ({
  userIdIdx: index("chat_thread_user_id_idx").on(table.userId),
}));
2. 选择性加载:
typescript
// 加载最小数据
selectThread(id){ id, title, userId, createdAt }

// 需要时加载完整数据
selectThreadDetails(id){ ...thread, messages, userPreferences }
3. SQL聚合:
typescript
// 获取包含最后消息时间戳的会话
const threadsWithActivity = await db
  .select({
    threadId: ChatThreadTable.id,
    lastMessageAt: sql<string>`MAX(${ChatMessageTable.createdAt})`,
  })
  .from(ChatThreadTable)
  .leftJoin(ChatMessageTable, eq(ChatThreadTable.id, ChatMessageTable.threadId))
  .groupBy(ChatThreadTable.id)
  .orderBy(desc(sql`last_message_at`));

Schema Evolution Workflow

Schema演进工作流

bash
undefined
bash
undefined

1. Modify schema in src/lib/db/pg/schema.pg.ts

1. 修改src/lib/db/pg/schema.pg.ts中的schema

export const NewTable = pgTable("new_table", { ... });
export const NewTable = pgTable("new_table", { ... });

2. Generate migration

2. 生成迁移

pnpm db:generate
pnpm db:generate

3. Review generated SQL in drizzle/migrations/

3. 查看drizzle/migrations/中生成的SQL

4. Apply migration

4. 应用迁移

pnpm db:migrate
pnpm db:migrate

5. Optional: Visual DB exploration

5. 可选:可视化数据库探索

pnpm db:studio

---
pnpm db:studio

---

Architectural Principles

架构原则

1. Progressive Enhancement

1. 渐进增强

Features build in layers:
Base Layer: Chat + LLM
Tool Layer: Default + MCP
Composition Layer: Workflows (tools as nodes)
Personalization Layer: Agents (workflows + prompts)
Evidence:
  • Agents can have
    instructions.mentions
    (inject tools/workflows)
  • Workflows can call MCP + default tools
  • Chat API composes all three tiers
User Journey:
  1. Start with default tools (no setup)
  2. Add MCP servers for specialized needs
  3. Combine into workflows for automation
  4. Package into agents for personas
功能分层构建:
基础层:聊天 + LLM
工具层:默认工具 + MCP
组合层:工作流(工具作为节点)
个性化层:Agents(工作流 + 提示词)
证据:
  • Agents可拥有
    instructions.mentions
    (注入工具/工作流)
  • 工作流可调用MCP + 默认工具
  • 聊天API组合所有三层
用户旅程:
  1. 从默认工具开始(无需设置)
  2. 添加MCP服务器满足特定需求
  3. 组合为工作流实现自动化
  4. 打包为Agents打造专属角色

2. Convention Over Configuration

2. 约定优于配置

New Tool?
  • Add to
    AppDefaultToolkit
    enum → auto-available
New Workflow Node?
  • Add to
    NodeKind
    enum → executor handles it
New MCP Server?
  • Just configure via UI → manager handles lifecycle
新工具?
  • 添加到
    AppDefaultToolkit
    枚举 → 自动可用
新工作流节点?
  • 添加到
    NodeKind
    枚举 → 执行器自动处理
新MCP服务器?
  • 仅需通过UI配置 → 管理器处理生命周期

3. Defensive Programming

3. 防御式编程

Use
safe()
everywhere
:
typescript
const tools = await safe(() => loadMcpTools())
  .orElse({});  // Returns default on failure
Philosophy: Never crash the chat - degrade gracefully
处处使用
safe()
:
typescript
const tools = await safe(() => loadMcpTools())
  .orElse({});  // 失败时返回默认值
理念:绝不导致聊天崩溃 - 优雅降级

4. Streaming-First

4. 优先流式处理

Evidence:
  • Chat API uses
    createUIMessageStream()
  • Workflow execution streams intermediate steps
  • Tool calls stream progress updates
Why: Live feedback, better UX, handles long operations
证据:
  • 聊天API使用
    createUIMessageStream()
  • 工作流执行流式传输中间步骤
  • 工具调用流式传输进度更新
优势:实时反馈,更好的用户体验,处理长时间操作

5. Type-Driven Development

5. 类型驱动开发

Pattern:
typescript
// Zod defines runtime validation AND TypeScript types
const schema = z.object({ name: z.string() });
type SchemaType = z.infer<typeof schema>;

// Discriminated unions for polymorphic data
type WorkflowNodeData =
  | { kind: "input"; ... }
  | { kind: "llm"; ... }
  | { kind: "tool"; ... };

// Brand types for runtime checking
VercelAIMcpToolTag.isMaybe(tool)

模式:
typescript
// Zod定义运行时校验与TypeScript类型
const schema = z.object({ name: z.string() });
type SchemaType = z.infer<typeof schema>;

// 多态数据的区分联合类型
type WorkflowNodeData =
  | { kind: "input"; ... }
  | { kind: "llm"; ... }
  | { kind: "tool"; ... };

// 用于运行时检查的品牌类型
VercelAIMcpToolTag.isMaybe(tool)

Extension Points Reference

扩展点参考

Quick lookup: "I want to add X" → "Modify Y file"
Want to add...Extend/Modify...File LocationNotes
New default tool
AppDefaultToolkit
enum +
APP_DEFAULT_TOOL_KIT
lib/ai/tools/tool-kit.ts
Add tool implementation in
lib/ai/tools/[category]/
+ rendering in
components/tool-invocation/
New tool category
AppDefaultToolkit
enum
lib/ai/tools/index.ts
Creates new toolkit group (e.g., Visualization, WebSearch)
New workflow node type
NodeKind
enum + executor + validator
lib/ai/workflow/workflow.interface.ts
+
executor/node-executor.ts
+
validator/node-validate.ts
Also add UI config in
components/workflow/node-config/
New API endpointCreate route handler
src/app/api/[resource]/route.ts
Follow standard pattern: auth → validation → repository → response
New server actionUse
validatedActionWithUser
src/app/api/[resource]/actions.ts
Import from
lib/action-utils.ts
New database tableAdd to schema + repository
lib/db/pg/schema.pg.ts
+
lib/db/pg/repositories/[name]-repository.pg.ts
Then
pnpm db:generate
and
pnpm db:migrate
New UI componentCreate in domain folder
src/components/[domain]/[name].tsx
Use shadcn/ui primitives from
components/ui/
New React hookCreate with
use-
prefix
src/hooks/use-[name].ts
or
src/hooks/queries/use-[name].ts
Data fetching hooks go in
queries/
subfolder
New Zod schemaAdd to validations
src/lib/validations/[domain].ts
Use
z.infer<typeof schema>
for TypeScript types
New AI providerAdd to providers registry
lib/ai/providers.ts
Use
createOpenAI
,
createAnthropic
, etc. from AI SDK
New MCP serverConfigure via UISettings → MCP ServersNo code changes needed (file or DB storage)
New agent templateCreate via UIAgents pageCombine tools/workflows/prompts
New permission typeAdd to permissions enum
lib/auth/permissions.ts
Use in
validatedActionWithAdminPermission
New E2E testAdd test file
tests/[feature].spec.ts
Use Playwright, follow existing patterns
New system promptAdd to prompts
lib/ai/prompts.ts
Use
mergeSystemPrompt
for composition
快速查找:"我想添加X" → "修改Y文件"
想添加...扩展/修改...文件位置说明
新默认工具
AppDefaultToolkit
枚举 +
APP_DEFAULT_TOOL_KIT
lib/ai/tools/tool-kit.ts
lib/ai/tools/[category]/
中实现工具 + 在
components/tool-invocation/
中添加渲染
新工具分类
AppDefaultToolkit
枚举
lib/ai/tools/index.ts
创建新的工具包组(如:Visualization、WebSearch)
新工作流节点类型
NodeKind
枚举 + 执行器 + 验证器
lib/ai/workflow/workflow.interface.ts
+
executor/node-executor.ts
+
validator/node-validate.ts
同时在
components/workflow/node-config/
中添加UI配置
新API端点创建路由处理器
src/app/api/[resource]/route.ts
遵循标准模式:认证 → 校验 → 仓库 → 响应
新服务器操作使用
validatedActionWithUser
src/app/api/[resource]/actions.ts
lib/action-utils.ts
导入
新数据库表添加表 + 仓库
lib/db/pg/
然后执行
pnpm db:generate
pnpm db:migrate
新UI组件在领域文件夹中创建
src/components/[domain]/[name].tsx
使用
components/ui/
中的shadcn/ui基础组件
新React钩子创建带
use-
前缀的文件
src/hooks/use-[name].ts
src/hooks/queries/use-[name].ts
数据获取钩子放在
queries/
子文件夹
新Zod校验规则添加到校验文件夹
src/lib/validations/[domain].ts
使用
z.infer<typeof schema>
获取TypeScript类型
新AI提供商添加到提供商注册表
lib/ai/providers.ts
使用AI SDK的
createOpenAI
createAnthropic
等方法
新MCP服务器通过UI配置设置 → MCP服务器无需代码变更(文件或数据库存储)
新Agent模板通过UI创建Agents页面组合工具/工作流/提示词
新权限类型添加到权限枚举
lib/auth/permissions.ts
validatedActionWithAdminPermission
中使用
新E2E测试添加测试文件
tests/[feature].spec.ts
使用Playwright,遵循现有模式
新系统提示词添加到提示词文件
lib/ai/prompts.ts
使用
mergeSystemPrompt
进行组合

Common Development Flows

常见开发流程

Adding a Feature End-to-End:
1. Define types (src/types/[domain].ts)
2. Create Zod schema (lib/validations/[domain].ts)
3. Add DB table + repository (lib/db/pg/)
4. Create API route (app/api/[resource]/route.ts)
5. Create UI component (components/[domain]/)
6. Create data hook (hooks/queries/use-[resource].ts)
7. Add E2E test (tests/[feature].spec.ts)
8. Run: pnpm check && pnpm test:e2e
Adding a Tool End-to-End:
1. Implement tool (lib/ai/tools/[category]/[name].ts)
2. Add to toolkit (lib/ai/tools/tool-kit.ts)
3. Create rendering component (components/tool-invocation/[name].tsx)
4. Add to tool invocation switch (components/tool-invocation/index.tsx)
5. Test with @toolname mention in chat
Adding a Workflow Node End-to-End:
1. Add NodeKind enum (lib/ai/workflow/workflow.interface.ts)
2. Define node data type (same file)
3. Add executor (lib/ai/workflow/executor/node-executor.ts)
4. Add validator (lib/ai/workflow/validator/node-validate.ts)
5. Create UI config (components/workflow/node-config/[name]-node.tsx)
6. Test in workflow builder

端到端添加功能:
1. 定义类型(src/types/[domain].ts)
2. 创建Zod校验规则(lib/validations/[domain].ts)
3. 添加数据库表 + 仓库(lib/db/pg/)
4. 创建API路由(app/api/[resource]/route.ts)
5. 创建UI组件(components/[domain]/)
6. 创建数据钩子(hooks/queries/use-[resource].ts)
7. 添加E2E测试(tests/[feature].spec.ts)
8. 运行:pnpm check && pnpm test:e2e
端到端添加工具:
1. 实现工具(lib/ai/tools/[category]/[name].ts)
2. 添加到工具包(lib/ai/tools/tool-kit.ts)
3. 创建渲染组件(components/tool-invocation/[name].tsx)
4. 添加到工具调用开关(components/tool-invocation/index.tsx)
5. 在聊天中使用@toolname提及进行测试
端到端添加工作流节点:
1. 添加NodeKind枚举(lib/ai/workflow/workflow.interface.ts)
2. 定义节点数据类型(同一文件)
3. 添加执行器(lib/ai/workflow/executor/node-executor.ts)
4. 添加验证器(lib/ai/workflow/validator/node-validate.ts)
5. 创建UI配置(components/workflow/node-config/[name]-node.tsx)
6. 在工作流构建器中测试

UX Patterns & @Mention System

UX模式与@提及系统

The @Mention Philosophy

@提及理念

Core Design Principle: Every feature is instantly accessible via
@mentions
- no digging through menus.
Why This Matters: Users can compose features on-the-fly without context switching.
核心设计原则:每个功能都可通过
@mentions
即时访问 - 无需在菜单中查找。
为何重要:用户无需切换上下文即可即时组合功能。

Three Types of @Mentions

三种@提及类型

1. @tool (Default Tools)

1. @tool(默认工具)

Format:
@tool("tool_name")
Examples:
@tool("web-search") find recent AI papers
@tool("create-table") show sales data
@tool("python-execution") calculate fibonacci
How It Works:
  • Parsed from message on server
  • Loads corresponding tools from
    APP_DEFAULT_TOOL_KIT
  • LLM decides when to invoke based on prompt
Use Case: Built-in capabilities (search, visualization, code execution)
格式
@tool("tool_name")
示例:
@tool("web-search") 查找最新AI论文
@tool("create-table") 展示销售数据
@tool("python-execution") 计算斐波那契数列
工作原理:
  • 在服务器端从消息中解析
  • APP_DEFAULT_TOOL_KIT
    加载对应工具
  • LLM根据提示词决定何时调用
使用场景:内置能力(搜索、可视化、代码执行)

2. @mcp (MCP Server Tools)

2. @mcp(MCP服务器工具)

Format:
@mcp("server_name")
or specific tool
@mcp("server_name:tool_name")
Examples:
@mcp("github") create an issue in my repo
@mcp("playwright") navigate to google.com
@mcp("slack:send-message") post update to #general
How It Works:
  • Mentions filter which MCP servers/tools to load
  • Reduces token usage (only relevant tools sent to LLM)
  • MCP manager handles connection and execution
Use Case: External integrations (GitHub, Slack, databases, etc.)
格式
@mcp("server_name")
或特定工具
@mcp("server_name:tool_name")
示例:
@mcp("github") 在我的仓库中创建issue
@mcp("playwright") 导航到google.com
@mcp("slack:send-message") 向#general频道发送更新
工作原理:
  • 提及内容过滤要加载的MCP服务器/工具
  • 减少token使用(仅向LLM发送相关工具)
  • MCP管理器处理连接与执行
使用场景:外部集成(GitHub、Slack、数据库等)

3. @workflow (Custom Workflows)

3. @workflow(自定义工作流)

Format:
@workflow("workflow_name")
Examples:
@workflow("customer-onboarding") process new signup
@workflow("data-pipeline") transform and analyze CSV
How It Works:
  • Workflows are converted to callable tools
  • LLM sees workflow as a single tool with description
  • Execution streams intermediate node results
Use Case: Multi-step automations, business processes
格式
@workflow("workflow_name")
示例:
@workflow("customer-onboarding") 处理新用户注册
@workflow("data-pipeline") 转换并分析CSV数据
工作原理:
  • 工作流转换为可调用工具
  • LLM将工作流视为单个带描述的工具
  • 执行时流式传输中间节点结果
使用场景:多步骤自动化、业务流程

4. @agent (Agent Personas)

4. @agent(Agent角色)

Format: Select agent from dropdown (not typed in message)
How It Works:
  • Agent's
    instructions.mentions
    auto-inject tools/workflows
  • System prompt prepended to conversation
  • Presets can override model/temperature
Use Case: Role-specific contexts (coding assistant, data analyst, etc.)
格式:从下拉菜单选择Agent(无需在消息中输入)
工作原理:
  • Agent的
    instructions.mentions
    自动注入工具/工作流
  • 系统提示词前置到对话中
  • 预设可覆盖模型/温度参数
使用场景:特定角色上下文(编码助手、数据分析师等)

Tool Choice Modes

工具选择模式

Context: User selects mode from dropdown
上下文:用户从下拉菜单选择模式

Auto Mode (Default)

自动模式(默认)

  • LLM can invoke tools autonomously
  • Multiple tool calls per message
  • Best for: Automation, workflows, exploration
Example Flow:
User: @tool("web-search") find AI news, then @tool("create-table") summarize
→ LLM searches → formats results → creates table → returns message
  • LLM可自主调用工具
  • 每条消息可进行多次工具调用
  • 最佳场景:自动化、工作流、探索
示例流程:
用户:@tool("web-search") 查找AI新闻,然后@tool("create-table") 汇总结果
→ LLM搜索 → 格式化结果 → 创建表格 → 返回消息

Manual Mode

手动模式

  • LLM proposes tool calls, waits for user approval
  • User sees "Tool: web-search" with args, clicks "Execute"
  • Best for: Sensitive operations, learning, debugging
Example Flow:
User: @mcp("github") create issue
→ LLM proposes: create_issue(repo="...", title="...", body="...")
→ User reviews and clicks "Execute"
→ Tool runs → result shown
  • LLM提议工具调用,等待用户确认
  • 用户看到"Tool: web-search"及参数,点击"Execute"
  • 最佳场景:敏感操作、学习、调试
示例流程:
用户:@mcp("github") 创建issue
→ LLM提议:create_issue(repo="...", title="...", body="...")
→ 用户审核并点击"Execute"
→ 工具运行 → 显示结果

None Mode

无工具模式

  • No tools loaded (text-only conversation)
  • Reduces latency and token usage
  • Best for: Brainstorming, explanations, simple queries
  • 不加载工具(纯文本对话)
  • 降低延迟与token使用
  • 最佳场景:头脑风暴、解释、简单查询

Preset System

预设系统

What Are Presets?
  • Quick configurations for common scenarios
  • Stored per-user
  • Can override: model, temperature, toolChoice, allowed MCP servers
Example Use Cases:
Preset: "Quick Chat"
- Model: GPT-4o-mini (fast)
- Tools: None
- Use for: Rapid Q&A

Preset: "Research Assistant"
- Model: Claude Sonnet 4.5
- Tools: @tool("web-search"), @mcp("wikipedia")
- Use for: Deep research

Preset: "Code Review"
- Model: GPT-5
- Tools: @mcp("github"), @tool("python-execution")
- Use for: Reviewing PRs with tests
How To Create:
  1. Configure chat (model, tools, settings)
  2. Click "Save as Preset"
  3. Name it
  4. Select from dropdown in future chats
什么是预设?
  • 常见场景的快速配置
  • 按用户存储
  • 可覆盖:模型、温度、工具选择、允许的MCP服务器
示例使用场景:
预设:"快速聊天"
- 模型:GPT-4o-mini(快速)
- 工具:无
- 用途:快速问答

预设:"研究助手"
- 模型:Claude Sonnet 4.5
- 工具:@tool("web-search")、@mcp("wikipedia")
- 用途:深度研究

预设:"代码审查"
- 模型:GPT-5
- 工具:@mcp("github")、@tool("python-execution")
- 用途:通过测试审查PR
创建方式:
  1. 配置聊天(模型、工具、设置)
  2. 点击"保存为预设"
  3. 命名
  4. 未来聊天时从下拉菜单选择

User Journey Examples

用户旅程示例

Beginner: First-Time User

新手:首次使用

1. Start chat (no @mentions) → Default tools available
2. Ask: "Search for news about AI"
3. LLM automatically uses @tool("web-search")
4. User sees: Search results → Formatted answer
5. Learns: Tools work automatically in Auto mode
1. 开始聊天(无@提及)→ 默认工具可用
2. 提问:"搜索AI相关新闻"
3. LLM自动使用@tool("web-search")
4. 用户看到:搜索结果 → 格式化答案
5. 了解:自动模式下工具自动工作

Intermediate: Using Workflows

中级:使用工作流

1. Create workflow in Workflow Builder:
   Input → Web Search → LLM Summary → Output
2. Save as "research-workflow"
3. In chat: "@workflow('research-workflow') AI trends 2025"
4. Sees: Live progress per node
5. Gets: Formatted research report
1. 在工作流构建器中创建工作流:
   输入 → 网页搜索 → LLM总结 → 输出
2. 保存为"research-workflow"
3. 在聊天中:"@workflow('research-workflow') 2025年AI趋势"
4. 看到:每个节点的实时进度
5. 获取:格式化的研究报告

Advanced: Agent + MCP + Workflows

高级:Agent + MCP + 工作流

1. Create agent "DevOps Assistant"
2. Agent instructions include: @mcp("github"), @workflow("deploy-pipeline")
3. Select agent from dropdown
4. Chat: "Deploy latest commit to staging"
5. Agent: Uses GitHub MCP → triggers deploy workflow → monitors → reports
1. 创建Agent "DevOps助手"
2. Agent指令包含:@mcp("github")、@workflow("deploy-pipeline")
3. 从下拉菜单选择该Agent
4. 聊天:"将最新提交部署到 staging 环境"
5. Agent:使用GitHub MCP → 触发部署工作流 → 监控 → 报告

Design Patterns Developers Should Follow

开发者应遵循的设计模式

1. Discoverability
  • Every tool should have clear description (shown in LLM context)
  • Use semantic names (
    create-table
    not
    tool-42
    )
2. Composability
  • Tools should be single-purpose
  • Workflows compose tools
  • Agents compose workflows + tools + context
3. Progressive Disclosure
  • Beginners: Auto mode, no @mentions (use defaults)
  • Intermediate: Explicit @tool/@mcp mentions
  • Advanced: Workflows, agents, presets
4. Feedback
  • Streaming for long operations
  • Progress updates for workflows
  • Clear error messages with solutions

1. 可发现性
  • 每个工具应有清晰描述(显示在LLM上下文中)
  • 使用语义化名称(
    create-table
    而非
    tool-42
2. 可组合性
  • 工具应单一职责
  • 工作流组合工具
  • Agents组合工作流 + 工具 + 上下文
3. 渐进式披露
  • 新手:自动模式,无@提及(使用默认值)
  • 中级:显式@tool/@mcp提及
  • 高级:工作流、Agents、预设
4. 反馈
  • 长时间操作使用流式处理
  • 工作流提供进度更新
  • 清晰的错误消息及解决方案

Practical Templates

实用模板

Template: Adding a New Default Tool

模板:添加新默认工具

typescript
// 1. Define in lib/ai/tools/[category]/[tool-name].ts
import { tool as createTool } from "ai";
import { z } from "zod";

export const myNewTool = createTool({
  description: "Clear description for LLM to understand when to use this",
  inputSchema: z.object({
    param: z.string().describe("What this parameter does"),
  }),
  execute: async (params) => {
    // For visualization tools: return "Success"
    // For data tools: return actual data
    return "Success";
  },
});

// 2. Add to lib/ai/tools/tool-kit.ts
import { DefaultToolName } from "./index";
import { myNewTool } from "./[category]/[tool-name]";

export enum DefaultToolName {
  // ... existing
  MyNewTool = "my_new_tool",
}

export const APP_DEFAULT_TOOL_KIT = {
  [AppDefaultToolkit.MyCategory]: {
    [DefaultToolName.MyNewTool]: myNewTool,
  },
};

// 3. Create rendering in components/tool-invocation/my-tool-invocation.tsx
export function MyToolInvocation({ part }: { part: ToolUIPart }) {
  const args = part.input as z.infer<typeof myNewTool.inputSchema>;
  return <div>{/* Render based on args */}</div>;
}

// 4. Add to components/tool-invocation/index.tsx switch
if (toolName === DefaultToolName.MyTool) {
  return <MyToolInvocation part={part} />;
}
typescript
// 1. 在lib/ai/tools/[category]/[tool-name].ts中定义
import { tool as createTool } from "ai";
import { z } from "zod";

export const myNewTool = createTool({
  description: "让LLM清楚理解何时使用此工具的清晰描述",
  inputSchema: z.object({
    param: z.string().describe("此参数的作用"),
  }),
  execute: async (params) => {
    // 可视化工具:返回"Success"
    // 数据工具:返回实际数据
    return "Success";
  },
});

// 2. 添加到lib/ai/tools/tool-kit.ts
import { DefaultToolName } from "./index";
import { myNewTool } from "./[category]/[tool-name]";

export enum DefaultToolName {
  // ... 现有枚举
  MyNewTool = "my_new_tool",
}

export const APP_DEFAULT_TOOL_KIT = {
  [AppDefaultToolkit.MyCategory]: {
    [DefaultToolName.MyNewTool]: myNewTool,
  },
};

// 3. 在components/tool-invocation/my-tool-invocation.tsx中创建渲染组件
export function MyToolInvocation({ part }: { part: ToolUIPart }) {
  const args = part.input as z.infer<typeof myNewTool.inputSchema>;
  return <div>{/* 根据参数渲染 */}</div>;
}

// 4. 添加到components/tool-invocation/index.tsx的开关中
if (toolName === DefaultToolName.MyTool) {
  return <MyToolInvocation part={part} />;
}

Template: Adding a New API Route

模板:添加新API路由

typescript
// src/app/api/[resource]/route.ts
import { getSession } from "auth/server";
import { [resource]Repository } from "lib/db/repository";
import { z } from "zod";

const querySchema = z.object({
  limit: z.coerce.number().default(10),
});

export async function GET(request: Request) {
  // 1. Auth check
  const session = await getSession();
  if (!session?.user.id) {
    return new Response("Unauthorized", { status: 401 });
  }

  // 2. Parse & validate
  try {
    const url = new URL(request.url);
    const params = querySchema.parse(Object.fromEntries(url.searchParams));

    // 3. Use repository
    const data = await [resource]Repository.selectByUserId(
      session.user.id,
      params.limit
    );

    return Response.json(data);
  } catch (error) {
    if (error instanceof z.ZodError) {
      return Response.json(
        { error: "Invalid params", details: error.message },
        { status: 400 }
      );
    }
    console.error("Failed:", error);
    return new Response("Internal Server Error", { status: 500 });
  }
}

export async function POST(request: Request) {
  const session = await getSession();
  if (!session?.user.id) {
    return new Response("Unauthorized", { status: 401 });
  }

  try {
    const body = await request.json();
    const data = createSchema.parse(body);

    const item = await [resource]Repository.insert({
      ...data,
      userId: session.user.id,
    });

    return Response.json(item);
  } catch (error) {
    if (error instanceof z.ZodError) {
      return Response.json({ error: "Invalid input" }, { status: 400 });
    }
    return Response.json({ error: "Internal error" }, { status: 500 });
  }
}
typescript
// src/app/api/[resource]/route.ts
import { getSession } from "auth/server";
import { [resource]Repository } from "lib/db/repository";
import { z } from "zod";

const querySchema = z.object({
  limit: z.coerce.number().default(10),
});

export async function GET(request: Request) {
  // 1. 认证检查
  const session = await getSession();
  if (!session?.user.id) {
    return new Response("Unauthorized", { status: 401 });
  }

  // 2. 解析 & 校验
  try {
    const url = new URL(request.url);
    const params = querySchema.parse(Object.fromEntries(url.searchParams));

    // 3. 使用仓库
    const data = await [resource]Repository.selectByUserId(
      session.user.id,
      params.limit
    );

    return Response.json(data);
  } catch (error) {
    if (error instanceof z.ZodError) {
      return Response.json(
        { error: "Invalid params", details: error.message },
        { status: 400 }
      );
    }
    console.error("Failed:", error);
    return new Response("Internal Server Error", { status: 500 });
  }
}

export async function POST(request: Request) {
  const session = await getSession();
  if (!session?.user.id) {
    return new Response("Unauthorized", { status: 401 });
  }

  try {
    const body = await request.json();
    const data = createSchema.parse(body);

    const item = await [resource]Repository.insert({
      ...data,
      userId: session.user.id,
    });

    return Response.json(item);
  } catch (error) {
    if (error instanceof z.ZodError) {
      return Response.json({ error: "Invalid input" }, { status: 400 });
    }
    return Response.json({ error: "Internal error" }, { status: 500 });
  }
}

Template: Adding a New Repository

模板:添加新仓库

typescript
// 1. Define interface in src/types/[domain].ts
export type MyRepository = {
  selectById(id: string): Promise<MyType | null>;
  selectByUserId(userId: string, limit?: number): Promise<MyType[]>;
  insert(data: InsertType): Promise<MyType>;
  update(id: string, data: Partial<InsertType>): Promise<MyType>;
  delete(id: string): Promise<void>;
};

// 2. Add table to src/lib/db/pg/schema.pg.ts
export const MyTable = pgTable("my_table", {
  id: uuid("id").primaryKey().defaultRandom(),
  userId: uuid("user_id").references(() => UserTable.id).notNull(),
  name: text("name").notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
}, (table) => ({
  userIdIdx: index("my_table_user_id_idx").on(table.userId),
}));

// 3. Implement in src/lib/db/pg/repositories/my-repository.pg.ts
import { pgDb as db } from "../db.pg";
import { MyTable } from "../schema.pg";
import { eq, desc } from "drizzle-orm";

export const pgMyRepository: MyRepository = {
  selectById: async (id) => {
    const [result] = await db
      .select()
      .from(MyTable)
      .where(eq(MyTable.id, id));
    return result ?? null;
  },

  selectByUserId: async (userId, limit = 10) => {
    return await db
      .select()
      .from(MyTable)
      .where(eq(MyTable.userId, userId))
      .orderBy(desc(MyTable.createdAt))
      .limit(limit);
  },

  insert: async (data) => {
    const [result] = await db
      .insert(MyTable)
      .values(data)
      .returning();
    return result;
  },

  update: async (id, data) => {
    const [result] = await db
      .update(MyTable)
      .set(data)
      .where(eq(MyTable.id, id))
      .returning();
    return result;
  },

  delete: async (id) => {
    await db.delete(MyTable).where(eq(MyTable.id, id));
  },
};

// 4. Export from src/lib/db/repository.ts
export { pgMyRepository as myRepository } from "./pg/repositories/my-repository.pg";

// 5. Generate and run migration
// pnpm db:generate
// pnpm db:migrate

typescript
// 1. 在src/types/[domain].ts中定义接口
export type MyRepository = {
  selectById(id: string): Promise<MyType | null>;
  selectByUserId(userId: string, limit?: number): Promise<MyType[]>;
  insert(data: InsertType): Promise<MyType>;
  update(id: string, data: Partial<InsertType>): Promise<MyType>;
  delete(id: string): Promise<void>;
};

// 2. 在src/lib/db/pg/schema.pg.ts中添加表
export const MyTable = pgTable("my_table", {
  id: uuid("id").primaryKey().defaultRandom(),
  userId: uuid("user_id").references(() => UserTable.id).notNull(),
  name: text("name").notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
}, (table) => ({
  userIdIdx: index("my_table_user_id_idx").on(table.userId),
}));

// 3. 在src/lib/db/pg/repositories/my-repository.pg.ts中实现
import { pgDb as db } from "../db.pg";
import { MyTable } from "../schema.pg";
import { eq, desc } from "drizzle-orm";

export const pgMyRepository: MyRepository = {
  selectById: async (id) => {
    const [result] = await db
      .select()
      .from(MyTable)
      .where(eq(MyTable.id, id));
    return result ?? null;
  },

  selectByUserId: async (userId, limit = 10) => {
    return await db
      .select()
      .from(MyTable)
      .where(eq(MyTable.userId, userId))
      .orderBy(desc(MyTable.createdAt))
      .limit(limit);
  },

  insert: async (data) => {
    const [result] = await db
      .insert(MyTable)
      .values(data)
      .returning();
    return result;
  },

  update: async (id, data) => {
    const [result] = await db
      .update(MyTable)
      .set(data)
      .where(eq(MyTable.id, id))
      .returning();
    return result;
  },

  delete: async (id) => {
    await db.delete(MyTable).where(eq(MyTable.id, id));
  },
};

// 4. 从src/lib/db/repository.ts导出
export { pgMyRepository as myRepository } from "./pg/repositories/my-repository.pg";

// 5. 生成并运行迁移
// pnpm db:generate
// pnpm db:migrate

Server Action Validators & Coding Standards

服务器操作验证器与编码标准

Server Action Validators (
lib/action-utils.ts
)

服务器操作验证器(
lib/action-utils.ts

Centralized pattern for validated, permission-gated server actions:
typescript
// Pattern 1: Simple validation
validatedAction(schema, async (data, formData) => { ... })

// Pattern 2: With user context (auto-auth, auto-error handling)
validatedActionWithUser(schema, async (data, formData, user) => { ... })

// Pattern 3: Permission-based (admin, user-manage)
validatedActionWithAdminPermission(schema, async (data, formData, session) => { ... })
Prevents:
  • Forgetting auth checks ✓
  • Inconsistent validation ✓
  • FormData parsing errors ✓
  • Non-standard error responses ✓
2. Tool Abstraction System Unified interface for multiple tool types using branded type tags:
typescript
// Branded types for runtime type narrowing
VercelAIMcpToolTag.create(tool)        // Brand as MCP tool
VercelAIWorkflowToolTag.isMaybe(tool)  // Check if Workflow tool

// Single handler for multiple tool types
if (VercelAIWorkflowToolTag.isMaybe(tool)) {
  // Workflow-specific logic
} else if (VercelAIMcpToolTag.isMaybe(tool)) {
  // MCP-specific logic
}
Tool Types:
  • MCP Tools: Model Context Protocol integrations
  • Workflow Tools: Visual DAG-based workflows
  • Default Tools: Built-in capabilities (search, code execution, etc.)
3. Workflow Execution Engine DAG-based workflow system with real-time streaming:
  • Streams node execution progress via
    dataStream.write()
  • Tracks: status, input/output, errors, timing
  • Token optimization: history stored without detailed results
4. State Management Zustand stores with shallow comparison for workflows and app config.

集中式模式用于经过校验、权限管控的服务器操作:
typescript
// 模式1:简单校验
validatedAction(schema, async (data, formData) => { ... })

// 模式2:带用户上下文(自动认证、自动错误处理)
validatedActionWithUser(schema, async (data, formData, user) => { ... })

// 模式3:基于权限(管理员、用户管理)
validatedActionWithAdminPermission(schema, async (data, formData, session) => { ... })
预防问题:
  • 忘记认证检查 ✓
  • 校验不一致 ✓
  • FormData解析错误 ✓
  • 非标准错误响应 ✓
2. 工具抽象系统 使用品牌类型标签为多种工具类型提供统一接口:
typescript
// 用于运行时类型收窄的品牌类型
VercelAIMcpToolTag.create(tool)        // 标记为MCP工具
VercelAIWorkflowToolTag.isMaybe(tool)  // 检查是否为Workflow工具

// 多工具类型的单一处理器
if (VercelAIWorkflowToolTag.isMaybe(tool)) {
  // 工作流专属逻辑
} else if (VercelAIMcpToolTag.isMaybe(tool)) {
  // MCP专属逻辑
}
工具类型:
  • MCP工具: Model Context Protocol集成
  • 工作流工具: 基于可视化DAG的工作流
  • 默认工具: 内置能力(搜索、代码执行等)
3. 工作流执行引擎 基于DAG的工作流系统,支持实时流式处理:
  • 通过
    dataStream.write()
    流式传输节点执行进度
  • 跟踪:状态、输入/输出、错误、耗时
  • Token优化:历史记录存储时不包含详细结果
4. 状态管理 使用Zustand存储,针对工作流和应用配置使用浅比较。

Coding Standards

编码标准

Naming Conventions

命名规范

TypeConventionExample
ComponentsPascalCase
ChatBot.tsx
,
WorkflowBuilder.tsx
Component fileskebab-case or PascalCase
chat-bot.tsx
,
ChatBot.tsx
HookscamelCase with
use-
prefix
use-chat-bot.ts
,
use-workflow.ts
UtilitiescamelCase
action-utils.ts
,
shared.chat.ts
API routesNext.js convention
src/app/api/[resource]/route.ts
TypesDomain suffix
chat.ts
,
mcp.ts
,
workflow.ts
类型规范示例
组件PascalCase
ChatBot.tsx
,
WorkflowBuilder.tsx
组件文件kebab-case 或 PascalCase
chat-bot.tsx
,
ChatBot.tsx
钩子camelCase 带
use-
前缀
use-chat-bot.ts
,
use-workflow.ts
工具库camelCase
action-utils.ts
,
shared.chat.ts
API路由Next.js约定
src/app/api/[resource]/route.ts
类型带领域后缀
chat.ts
,
mcp.ts
,
workflow.ts

TypeScript Standards

TypeScript标准

  • Strict TypeScript throughout (no implicit any)
  • Zod for validation AND type inference:
    typescript
    const schema = z.object({ name: z.string() })
    type SchemaType = z.infer<typeof schema>
  • Custom type tags for runtime type narrowing (see Tool Abstraction)
  • Types organized by domain in
    src/types/
  • 严格TypeScript贯穿始终(无隐式any)
  • Zod用于校验与类型推断:
    typescript
    const schema = z.object({ name: z.string() })
    type SchemaType = z.infer<typeof schema>
  • 自定义类型标签用于运行时类型收窄(见工具抽象)
  • 类型按领域组织
    src/types/

Code Quality

代码质量

  • Line width: 80 characters
  • Indentation: 2 spaces
  • Formatter: Biome 1.9.4
  • Linter: Biome (no ESLint)
  • Validation: Zod everywhere (forms, API, dynamic config)
  • 行宽: 80字符
  • 缩进: 2空格
  • 格式化工具: Biome 1.9.4
  • 代码检查: Biome(无需ESLint)
  • 校验: 处处使用Zod(表单、API、动态配置)

Error Handling

错误处理

  • Enum error types for specific errors:
    typescript
    enum UpdateUserPasswordError {
      INVALID_CURRENT_PASSWORD = "invalid_current_password",
      PASSWORD_MISMATCH = "password_mismatch"
    }
  • Cross-field validation with Zod
    superRefine
    :
    typescript
    .superRefine((data, ctx) => {
      if (data.password !== data.confirmPassword) {
        ctx.addIssue({ path: ["confirmPassword"], message: "Passwords must match" })
      }
    })

  • 枚举错误类型用于特定错误:
    typescript
    enum UpdateUserPasswordError {
      INVALID_CURRENT_PASSWORD = "invalid_current_password",
      PASSWORD_MISMATCH = "password_mismatch"
    }
  • 跨字段校验使用Zod
    superRefine
    :
    typescript
    .superRefine((data, ctx) => {
      if (data.password !== data.confirmPassword) {
        ctx.addIssue({ path: ["confirmPassword"], message: "Passwords must match" })
      }
    })

Development Workflow

开发工作流

Core Commands

核心命令

bash
undefined
bash
undefined

Development

开发

pnpm dev # Start dev server pnpm build # Production build pnpm start # Start production server pnpm lint:fix # Auto-fix linting issues
pnpm dev # 启动开发服务器 pnpm build # 生产构建 pnpm start # 启动生产服务器 pnpm lint:fix # 自动修复代码检查问题

Database (Drizzle ORM)

数据库(Drizzle ORM)

pnpm db:generate # Generate migrations pnpm db:migrate # Run migrations pnpm db:push # Push schema changes pnpm db:studio # Open Drizzle Studio
pnpm db:generate # 生成迁移 pnpm db:migrate # 运行迁移 pnpm db:push # 推送schema变更 pnpm db:studio # 打开Drizzle Studio

Testing

测试

pnpm test # Run Vitest unit tests pnpm test:e2e # Full Playwright E2E suite pnpm test:e2e:first-user # First-user signup + admin role tests pnpm test:e2e:standard # Standard tests (skip first-user) pnpm test:e2e:ui # Interactive Playwright UI
pnpm test # 运行Vitest单元测试 pnpm test:e2e # 完整Playwright E2E测试套件 pnpm test:e2e:first-user # 首次用户注册 + 管理员角色测试 pnpm test:e2e:standard # 标准测试(跳过首次用户设置) pnpm test:e2e:ui # 交互式Playwright UI

Quality Check

质量检查

pnpm check # Run lint + type-check + tests
undefined
pnpm check # 运行代码检查 + 类型检查 + 测试
undefined

Environment Setup

环境设置

  • Copy
    .env.example
    to
    .env
    (auto-generated on
    pnpm i
    )
  • Required: PostgreSQL connection, at least one LLM API key
  • Optional: OAuth providers (Google, GitHub, Microsoft), Redis, Vercel Blob
  • 复制
    .env.example
    .env
    pnpm i
    时自动生成)
  • 必填:PostgreSQL连接、至少一个LLM API密钥
  • 可选:OAuth提供商(Google、GitHub、Microsoft)、Redis、Vercel Blob

Branch Strategy

分支策略

  • Main: Production-ready code
  • Feature branches:
    feat/feature-name
    or
    fix/bug-name
  • Squash merge: Single commit per PR for clean history

  • Main: 生产就绪代码
  • 功能分支:
    feat/feature-name
    fix/bug-name
  • ** squash merge**: 每个PR对应单个提交,保持历史干净

Testing Patterns

测试模式

Unit Tests (Vitest)

单元测试(Vitest)

  • Collocated with source code (
    *.test.ts
    )
  • Coverage: Happy path + one failure mode minimum
  • Example:
    typescript
    // src/lib/utils.test.ts
    import { describe, it, expect } from 'vitest'
    import { formatDate } from './utils'
    
    describe('formatDate', () => {
      it('formats ISO date correctly', () => {
        expect(formatDate('2025-01-01')).toBe('January 1, 2025')
      })
    
      it('handles invalid date', () => {
        expect(formatDate('invalid')).toBe('Invalid Date')
      })
    })
  • 与源代码同目录
    *.test.ts
  • 覆盖率: 至少覆盖正常路径 + 一种失败场景
  • 示例:
    typescript
    // src/lib/utils.test.ts
    import { describe, it, expect } from 'vitest'
    import { formatDate } from './utils'
    
    describe('formatDate', () => {
      it('formats ISO date correctly', () => {
        expect(formatDate('2025-01-01')).toBe('January 1, 2025')
      })
    
      it('handles invalid date', () => {
        expect(formatDate('invalid')).toBe('Invalid Date')
      })
    })

E2E Tests (Playwright)

E2E测试(Playwright)

Special orchestration for multi-user and first-user scenarios:
bash
undefined
多用户和首次用户场景的特殊编排:
bash
undefined

First-user tests (clean DB → signup → verify admin role)

首次用户测试(清空DB → 注册 → 验证管理员角色)

pnpm test:e2e:first-user
pnpm test:e2e:first-user

Standard tests (assumes first user exists)

标准测试(假设首次用户已存在)

pnpm test:e2e:standard
pnpm test:e2e:standard

Full suite (first-user → standard)

完整套件(首次用户设置 → 标准测试)

pnpm test:e2e

**Test project dependencies** ensure sequenced execution:
1. Clean database
2. Run first-user signup + role verification
3. Run standard multi-user tests

**Shared auth states** across test runs to avoid re-login.

**Seed/cleanup scripts** for deterministic testing.

---
pnpm test:e2e

**测试项目依赖**确保按顺序执行:
1. 清空数据库
2. 运行首次用户注册 + 角色验证
3. 运行标准多用户测试

**共享认证状态**在测试运行之间复用,避免重复登录。

**种子/清理脚本**用于确定性测试。

---

Contribution Guidelines

贡献指南

Before Starting

开始之前

Major changes require discussion first:
  • New UI components
  • New API endpoints
  • External service integrations
  • Breaking changes
No prior approval needed:
  • Bug fixes
  • Documentation improvements
  • Minor refactoring
重大变更需要先讨论:
  • 新UI组件
  • 新API端点
  • 外部服务集成
  • 破坏性变更
无需预先批准:
  • Bug修复
  • 文档改进
  • 小型重构

Pull Request Standards

拉取请求标准

Title format (Conventional Commits):
feat: Add realtime voice chat
fix: Resolve MCP tool streaming error
chore: Update dependencies
docs: Add OAuth setup guide
Prefixes:
feat:
,
fix:
,
chore:
,
docs:
,
style:
,
refactor:
,
test:
,
perf:
,
build:
Visual documentation required:
  • Before/after screenshots for UI changes
  • Screen recordings for interactive features
  • Mobile + desktop views for responsive updates
Description should explain:
  1. What changed
  2. Why it changed
  3. How you tested it
标题格式(Conventional Commits):
feat: 添加实时语音聊天
fix: 修复MCP工具流式错误
chore: 更新依赖
docs: 添加OAuth设置指南
前缀:
feat:
,
fix:
,
chore:
,
docs:
,
style:
,
refactor:
,
test:
,
perf:
,
build:
需要可视化文档:
  • UI变更的前后截图
  • 交互功能的屏幕录制
  • 响应式更新的移动端 + 桌面端视图
描述应说明:
  1. 变更内容
  2. 变更原因
  3. 测试方式

Pre-Submission Checklist

提交前检查清单

bash
undefined
bash
undefined

Must pass before PR:

PR提交前必须通过:

pnpm check # Lint + type-check + tests pnpm test:e2e # E2E tests (if applicable)

- [ ] Tests added for new features/bug fixes
- [ ] Visual documentation included (if UI change)
- [ ] Conventional Commit title
- [ ] Description explains what, why, testing

---
pnpm check # 代码检查 + 类型检查 + 测试 pnpm test:e2e # E2E测试(如适用)

- [ ] 为新功能/bug修复添加了测试
- [ ] 包含可视化文档(若为UI变更)
- [ ] 使用Conventional Commit标题
- [ ] 描述说明了变更内容、原因、测试方式

---

Critical Rules

关键规则

Always Do

必须遵守

✅ Use
validatedActionWithUser
or
validatedActionWithAdminPermission
for server actions ✅ Check tool types with branded type tags before execution ✅ Use Zod
superRefine
for cross-field validation ✅ Add unit tests (happy path + one failure mode) ✅ Run
pnpm check
before PR submission ✅ Include visual documentation for UI changes ✅ Use Conventional Commit format for PR titles ✅ Run E2E tests when touching critical flows
✅ 服务器操作使用
validatedActionWithUser
validatedActionWithAdminPermission
✅ 执行前使用品牌类型标签检查工具类型 ✅ 跨字段校验使用Zod
superRefine
✅ 添加单元测试(正常路径 + 一种失败场景) ✅ PR提交前运行
pnpm check
✅ UI变更包含可视化文档 ✅ PR标题使用Conventional Commit格式 ✅ 修改关键流程时运行E2E测试

Never Do

禁止操作

❌ Implement server actions without auth validators ❌ Assume tool type without runtime check ❌ Parse FormData manually (use validators) ❌ Mutate Zustand state directly (use shallow updates) ❌ Skip first-user tests on clean database ❌ Commit without running
pnpm check
❌ Submit PR without visual docs (if UI change) ❌ Use non-conventional commit format

❌ 服务器操作未使用认证验证器 ❌ 未进行运行时检查就假设工具类型 ❌ 手动解析FormData(使用验证器) ❌ 直接修改Zustand状态(使用浅更新) ❌ 在清空数据库时跳过首次用户测试 ❌ 未运行
pnpm check
就提交 ❌ UI变更未包含可视化文档就提交PR ❌ 使用非Conventional Commit格式

Known Issues Prevention

已知问题预防

This skill prevents 8 documented issues:
本技能可预防8个已记录的问题:

Issue #1: Forgetting Auth Checks in Server Actions

问题#1: 服务器操作忘记认证检查

Error: Unauthorized users accessing protected actions Why It Happens: Manual auth implementation is inconsistent Prevention: Use
validatedActionWithUser
or
validatedActionWithAdminPermission
typescript
// ❌ BAD: Manual auth check
export async function updateProfile(data: ProfileData) {
  const session = await getSession()
  if (!session) throw new Error("Unauthorized")
  // ... rest of logic
}

// ✅ GOOD: Use validator
export const updateProfile = validatedActionWithUser(
  profileSchema,
  async (data, formData, user) => {
    // user is guaranteed to exist, auto-error handling
  }
)
错误: 未授权用户访问受保护操作 原因: 手动认证实现不一致 预防: 使用
validatedActionWithUser
validatedActionWithAdminPermission
typescript
// ❌ 错误:手动认证检查
export async function updateProfile(data: ProfileData) {
  const session = await getSession()
  if (!session) throw new Error("Unauthorized")
  // ... 其余逻辑
}

// ✅ 正确:使用验证器
export const updateProfile = validatedActionWithUser(
  profileSchema,
  async (data, formData, user) => {
    // user已保证存在,自动错误处理
  }
)

Issue #2: Tool Type Mismatches

问题#2: 工具类型不匹配

Error: Runtime type errors when executing tools Why It Happens: Not checking tool type before execution Prevention: Use branded type tags for runtime narrowing
typescript
// ❌ BAD: Assuming tool type
const result = await executeMcpTool(tool)

// ✅ GOOD: Check tool type
if (VercelAIMcpToolTag.isMaybe(tool)) {
  const result = await executeMcpTool(tool)
} else if (VercelAIWorkflowToolTag.isMaybe(tool)) {
  const result = await executeWorkflowTool(tool)
}
错误: 执行工具时出现运行时类型错误 原因: 执行前未检查工具类型 预防: 使用品牌类型标签进行运行时收窄
typescript
// ❌ 错误:假设工具类型
const result = await executeMcpTool(tool)

// ✅ 正确:检查工具类型
if (VercelAIMcpToolTag.isMaybe(tool)) {
  const result = await executeMcpTool(tool)
} else if (VercelAIWorkflowToolTag.isMaybe(tool)) {
  const result = await executeWorkflowTool(tool)
}

Issue #3: FormData Parsing Errors

问题#3: FormData解析错误

Error: Inconsistent error handling for form submissions Why It Happens: Manual FormData parsing with ad-hoc validation Prevention: Validators handle parsing automatically
typescript
// ❌ BAD: Manual parsing
const name = formData.get("name") as string
if (!name) throw new Error("Name required")

// ✅ GOOD: Validator with Zod
const schema = z.object({ name: z.string().min(1) })
export const action = validatedAction(schema, async (data) => {
  // data.name is validated and typed
})
错误: 表单提交的错误处理不一致 原因: 手动解析FormData并使用临时校验 预防: 验证器自动处理解析
typescript
// ❌ 错误:手动解析
const name = formData.get("name") as string
if (!name) throw new Error("Name required")

// ✅ 正确:使用Zod验证器
const schema = z.object({ name: z.string().min(1) })
export const action = validatedAction(schema, async (data) => {
  // data.name已校验并带类型
})

Issue #4: Cross-Field Validation Issues

问题#4: 跨字段校验问题

Error: Password mismatch validation not working Why It Happens: Separate validation for related fields Prevention: Use Zod
superRefine
typescript
// ❌ BAD: Separate checks
if (data.password !== data.confirmPassword) { /* error */ }

// ✅ GOOD: Zod superRefine
const schema = z.object({
  password: z.string(),
  confirmPassword: z.string()
}).superRefine((data, ctx) => {
  if (data.password !== data.confirmPassword) {
    ctx.addIssue({
      path: ["confirmPassword"],
      message: "Passwords must match"
    })
  }
})
错误: 密码匹配校验不生效 原因: 相关字段分开校验 预防: 使用Zod
superRefine
typescript
// ❌ 错误:分开检查
if (data.password !== data.confirmPassword) { /* error */ }

// ✅ 正确:Zod superRefine
const schema = z.object({
  password: z.string(),
  confirmPassword: z.string()
}).superRefine((data, ctx) => {
  if (data.password !== data.confirmPassword) {
    ctx.addIssue({
      path: ["confirmPassword"],
      message: "Passwords must match"
    })
  }
})

Issue #5: Workflow State Inconsistency

问题#5: 工作流状态不一致

Error: Zustand state updates not triggering re-renders Why It Happens: Deep mutation of nested workflow state Prevention: Use shallow updates
typescript
// ❌ BAD: Deep mutation
store.workflow.nodes[0].status = "complete"

// ✅ GOOD: Shallow update
set(state => ({
  workflow: {
    ...state.workflow,
    nodes: state.workflow.nodes.map((node, i) =>
      i === 0 ? { ...node, status: "complete" } : node
    )
  }
}))
错误: Zustand状态更新未触发重渲染 原因: 深度修改嵌套工作流状态 预防: 使用浅更新
typescript
// ❌ 错误:深度修改
store.workflow.nodes[0].status = "complete"

// ✅ 正确:浅更新
set(state => ({
  workflow: {
    ...state.workflow,
    nodes: state.workflow.nodes.map((node, i) =>
      i === 0 ? { ...node, status: "complete" } : node
    )
  }
}))

Issue #6: Missing E2E Test Setup

问题#6: 缺少E2E测试设置

Error: E2E tests failing on clean database Why It Happens: Running standard tests before first-user setup Prevention: Use correct test commands
bash
undefined
错误: 清空数据库后E2E测试失败 原因: 运行标准测试前未完成首次用户设置 预防: 使用正确的测试命令
bash
undefined

❌ BAD: Running standard tests on clean DB

❌ 错误:在清空DB上运行标准测试

pnpm test:e2e:standard
pnpm test:e2e:standard

✅ GOOD: Full suite with first-user setup

✅ 正确:运行包含首次用户设置的完整套件

pnpm test:e2e
undefined
pnpm test:e2e
undefined

Issue #7: Environment Config Mistakes

问题#7: 环境配置错误

Error: Missing required environment variables causing crashes Why It Happens: Not copying
.env.example
to
.env
Prevention: Auto-generated
.env
on
pnpm i
bash
undefined
错误: 缺少必填环境变量导致崩溃 原因: 未将
.env.example
复制到
.env
预防:
pnpm i
时自动生成
.env
bash
undefined

Auto-generates .env on install

安装时自动生成.env

pnpm i
pnpm i

Verify all required vars present

验证所有必填变量是否存在

Required: DATABASE_URL, at least one LLM_API_KEY

必填:DATABASE_URL、至少一个LLM_API_KEY

undefined
undefined

Issue #8: Incorrect Commit Message Format

问题#8: 提交消息格式错误

Error: CI/CD failures due to non-conventional commit format Why It Happens: Not following Conventional Commits standard Prevention: Use prefix + colon format
bash
undefined
错误: CI/CD因非Conventional Commit格式失败 原因: 未遵循Conventional Commits标准 预防: 使用前缀+冒号格式
bash
undefined

❌ BAD:

❌ 错误:

git commit -m "added feature" git commit -m "fix bug"
git commit -m "added feature" git commit -m "fix bug"

✅ GOOD:

✅ 正确:

git commit -m "feat: add MCP tool streaming" git commit -m "fix: resolve auth redirect loop"

---
git commit -m "feat: add MCP tool streaming" git commit -m "fix: resolve auth redirect loop"

---

Common Patterns

常见模式

Pattern 1: Server Action with User Context

模式1: 带用户上下文的服务器操作

typescript
import { validatedActionWithUser } from "@/lib/action-utils"
import { z } from "zod"

const updateProfileSchema = z.object({
  name: z.string().min(1),
  email: z.string().email()
})

export const updateProfile = validatedActionWithUser(
  updateProfileSchema,
  async (data, formData, user) => {
    // user is guaranteed authenticated
    // data is validated and typed
    await db.update(users).set(data).where(eq(users.id, user.id))
    return { success: true }
  }
)
When to use: Any server action that requires authentication
typescript
import { validatedActionWithUser } from "@/lib/action-utils"
import { z } from "zod"

const updateProfileSchema = z.object({
  name: z.string().min(1),
  email: z.string().email()
})

export const updateProfile = validatedActionWithUser(
  updateProfileSchema,
  async (data, formData, user) => {
    // user已保证认证
    // data已校验并带类型
    await db.update(users).set(data).where(eq(users.id, user.id))
    return { success: true }
  }
)
适用场景: 任何需要认证的服务器操作

Pattern 2: Tool Type Checking

模式2: 工具类型检查

typescript
import { VercelAIMcpToolTag, VercelAIWorkflowToolTag } from "@/lib/ai/tools"

async function executeTool(tool: unknown) {
  if (VercelAIMcpToolTag.isMaybe(tool)) {
    return await executeMcpTool(tool)
  } else if (VercelAIWorkflowToolTag.isMaybe(tool)) {
    return await executeWorkflowTool(tool)
  } else {
    return await executeDefaultTool(tool)
  }
}
When to use: Handling multiple tool types in unified interface
typescript
import { VercelAIMcpToolTag, VercelAIWorkflowToolTag } from "@/lib/ai/tools"

async function executeTool(tool: unknown) {
  if (VercelAIWorkflowToolTag.isMaybe(tool)) {
    return await executeWorkflowTool(tool)
  } else if (VercelAIMcpToolTag.isMaybe(tool)) {
    return await executeMcpTool(tool)
  } else {
    return await executeDefaultTool(tool)
  }
}
适用场景: 在统一接口中处理多种工具类型

Pattern 3: Workflow State Updates

模式3: 工作流状态更新

typescript
import { useWorkflowStore } from "@/app/store/workflow"

// In component:
const updateNodeStatus = useWorkflowStore(state => state.updateNodeStatus)

// In store:
updateNodeStatus: (nodeId, status) =>
  set(state => ({
    workflow: {
      ...state.workflow,
      nodes: state.workflow.nodes.map(node =>
        node.id === nodeId ? { ...node, status } : node
      )
    }
  }))
When to use: Updating nested Zustand state without mutation

typescript
import { useWorkflowStore } from "@/app/store/workflow"

// 在组件中:
const updateNodeStatus = useWorkflowStore(state => state.updateNodeStatus)

// 在存储中:
updateNodeStatus: (nodeId, status) =>
  set(state => ({
    workflow: {
      ...state.workflow,
      nodes: state.workflow.nodes.map((node, i) =>
        i === 0 ? { ...node, status } : node
      )
    }
  }))
适用场景: 不修改嵌套Zustand状态的情况下进行更新

Using Bundled Resources

使用捆绑资源

References (references/)

参考资料(references/)

  • references/AGENTS.md
    - Full repository guidelines (loaded when detailed structure questions arise)
  • references/CONTRIBUTING.md
    - Complete contribution process (loaded when PR standards questions arise)
When Claude should load these: When user asks about detailed better-chatbot conventions, asks "what are the full guidelines?", or needs comprehensive contribution workflow details.

  • references/AGENTS.md
    - 完整仓库指南(当出现详细结构问题时加载)
  • references/CONTRIBUTING.md
    - 完整贡献流程(当出现PR标准问题时加载)
Claude何时加载这些资料: 当用户询问详细的better-chatbot规范、“完整指南是什么?”或需要全面的贡献工作流细节时。

Dependencies

依赖

Required:
  • next@15.3.2 - Framework
  • ai@5.0.82 - Vercel AI SDK
  • better-auth@1.3.34 - Authentication
  • drizzle-orm@0.41.0 - Database ORM
  • @modelcontextprotocol/sdk@1.20.2 - MCP support
  • zod@3.24.2 - Validation
  • zustand@5.0.3 - State management
Testing:
  • vitest@3.2.4 - Unit tests
  • @playwright/test@1.56.1 - E2E tests

必填:
  • next@15.3.2 - 框架
  • ai@5.0.82 - Vercel AI SDK
  • better-auth@1.3.34 - 认证
  • drizzle-orm@0.41.0 - 数据库ORM
  • @modelcontextprotocol/sdk@1.20.2 - MCP支持
  • zod@3.24.2 - 校验
  • zustand@5.0.3 - 状态管理
测试:
  • vitest@3.2.4 - 单元测试
  • @playwright/test@1.56.1 - E2E测试

Official Documentation

官方文档

Production Example

生产示例

This skill is based on better-chatbot production standards:
  • Live: https://betterchatbot.vercel.app
  • Tests: 48+ E2E tests passing
  • Errors: 0 (all 8 known issues prevented)
  • Validation: ✅ Multi-user scenarios, workflow execution, MCP tools

本技能基于better-chatbot生产标准:
  • 在线运行: https://betterchatbot.vercel.app
  • 测试: 48+个E2E测试通过
  • 错误: 0(所有8个已知问题已预防)
  • 验证: ✅ 多用户场景、工作流执行、MCP工具

Complete Setup Checklist

完整设置检查清单

When contributing to better-chatbot:
  • Fork and clone repository
  • Run
    pnpm i
    (auto-generates
    .env
    )
  • Configure required env vars (DATABASE_URL, LLM_API_KEY)
  • Run
    pnpm dev
    and verify it starts
  • Create feature branch
  • Add unit tests for new features
  • Run
    pnpm check
    before PR
  • Run
    pnpm test:e2e
    if touching critical flows
  • Include visual docs (screenshots/recordings)
  • Use Conventional Commit title
  • Squash merge when approved

Questions? Issues?
  1. Check
    references/AGENTS.md
    for detailed guidelines
  2. Check
    references/CONTRIBUTING.md
    for PR process
  3. Check official docs: https://github.com/cgoinglove/better-chatbot
  4. Ensure PostgreSQL and LLM API key are configured

Token Efficiency: ~60% savings | Errors Prevented: 8 | Production Verified: Yes
贡献better-chatbot时:
  • Fork并克隆仓库
  • 运行
    pnpm i
    (自动生成
    .env
  • 配置必填环境变量(DATABASE_URL、LLM_API_KEY)
  • 运行
    pnpm dev
    并验证启动成功
  • 创建功能分支
  • 为新功能添加单元测试
  • PR提交前运行
    pnpm check
  • 修改关键流程时运行
    pnpm test:e2e
  • 包含可视化文档(截图/录制)
  • 使用Conventional Commit标题
  • 批准后进行squash merge

有疑问?遇到问题?
  1. 查看
    references/AGENTS.md
    获取详细指南
  2. 查看
    references/CONTRIBUTING.md
    获取完整贡献流程
  3. 查看官方文档: https://github.com/cgoinglove/better-chatbot
  4. 确保PostgreSQL和LLM API密钥已配置

Token效率: ~60% 节省 | 预防错误: 8个 | 生产验证: 是