better-chatbot
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesebetter-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 migrationsbetter-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.tsThis 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.tsWhy: 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: for functional error handling
ts-safePhilosophy: 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 toolWhy 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_nameNode 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 back1. 请求 → /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:
- Add enum to
AppDefaultToolkit - Implement tool with
createTool() - Add to
APP_DEFAULT_TOOL_KIT - Tool automatically available via
@toolname
添加新工具类型很简单:
- 向添加枚举
AppDefaultToolkit - 使用实现工具
createTool() - 添加到
APP_DEFAULT_TOOL_KIT - 工具自动通过可用
@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 componentsPrinciple: Group by feature, not by type
位置:
src/components/components/
├── ui/ → shadcn/ui 基础组件
├── layouts/ → 应用结构
├── agent/ → Agent专属组件
├── workflow/ → 工作流编辑器
├── tool-invocation/ → 工具结果渲染
└── *.tsx → 共享组件原则:按功能分组,而非按类型
Compound Component Pattern
复合组件模式
Example: +
message.tsxmessage-parts.tsxPhilosophy: 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.tsxmessage-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.tsxStructure:
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 migrationsPhilosophy: 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
undefinedbash
undefined1. 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 (inject tools/workflows)
instructions.mentions - Workflows can call MCP + default tools
- Chat API composes all three tiers
User Journey:
- Start with default tools (no setup)
- Add MCP servers for specialized needs
- Combine into workflows for automation
- Package into agents for personas
功能分层构建:
基础层:聊天 + LLM
↓
工具层:默认工具 + MCP
↓
组合层:工作流(工具作为节点)
↓
个性化层:Agents(工作流 + 提示词)证据:
- Agents可拥有(注入工具/工作流)
instructions.mentions - 工作流可调用MCP + 默认工具
- 聊天API组合所有三层
用户旅程:
- 从默认工具开始(无需设置)
- 添加MCP服务器满足特定需求
- 组合为工作流实现自动化
- 打包为Agents打造专属角色
2. Convention Over Configuration
2. 约定优于配置
New Tool?
- Add to enum → auto-available
AppDefaultToolkit
New Workflow Node?
- Add to enum → executor handles it
NodeKind
New MCP Server?
- Just configure via UI → manager handles lifecycle
新工具?
- 添加到枚举 → 自动可用
AppDefaultToolkit
新工作流节点?
- 添加到枚举 → 执行器自动处理
NodeKind
新MCP服务器?
- 仅需通过UI配置 → 管理器处理生命周期
3. Defensive Programming
3. 防御式编程
Use everywhere:
safe()typescript
const tools = await safe(() => loadMcpTools())
.orElse({}); // Returns default on failurePhilosophy: 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 Location | Notes |
|---|---|---|---|
| New default tool | | | Add tool implementation in |
| New tool category | | | Creates new toolkit group (e.g., Visualization, WebSearch) |
| New workflow node type | | | Also add UI config in |
| New API endpoint | Create route handler | | Follow standard pattern: auth → validation → repository → response |
| New server action | Use | | Import from |
| New database table | Add to schema + repository | | Then |
| New UI component | Create in domain folder | | Use shadcn/ui primitives from |
| New React hook | Create with | | Data fetching hooks go in |
| New Zod schema | Add to validations | | Use |
| New AI provider | Add to providers registry | | Use |
| New MCP server | Configure via UI | Settings → MCP Servers | No code changes needed (file or DB storage) |
| New agent template | Create via UI | Agents page | Combine tools/workflows/prompts |
| New permission type | Add to permissions enum | | Use in |
| New E2E test | Add test file | | Use Playwright, follow existing patterns |
| New system prompt | Add to prompts | | Use |
快速查找:"我想添加X" → "修改Y文件"
| 想添加... | 扩展/修改... | 文件位置 | 说明 |
|---|---|---|---|
| 新默认工具 | | | 在 |
| 新工具分类 | | | 创建新的工具包组(如:Visualization、WebSearch) |
| 新工作流节点类型 | | | 同时在 |
| 新API端点 | 创建路由处理器 | | 遵循标准模式:认证 → 校验 → 仓库 → 响应 |
| 新服务器操作 | 使用 | | 从 |
| 新数据库表 | 添加表 + 仓库 | | 然后执行 |
| 新UI组件 | 在领域文件夹中创建 | | 使用 |
| 新React钩子 | 创建带 | | 数据获取钩子放在 |
| 新Zod校验规则 | 添加到校验文件夹 | | 使用 |
| 新AI提供商 | 添加到提供商注册表 | | 使用AI SDK的 |
| 新MCP服务器 | 通过UI配置 | 设置 → MCP服务器 | 无需代码变更(文件或数据库存储) |
| 新Agent模板 | 通过UI创建 | Agents页面 | 组合工具/工作流/提示词 |
| 新权限类型 | 添加到权限枚举 | | 在 |
| 新E2E测试 | 添加测试文件 | | 使用Playwright,遵循现有模式 |
| 新系统提示词 | 添加到提示词文件 | | 使用 |
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:e2eAdding 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 chatAdding 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 - no digging through menus.
@mentionsWhy 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 fibonacciHow 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: or specific tool
@mcp("server_name")@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 #generalHow 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 CSVHow 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 auto-inject tools/workflows
instructions.mentions - 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 testsHow To Create:
- Configure chat (model, tools, settings)
- Click "Save as Preset"
- Name it
- 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创建方式:
- 配置聊天(模型、工具、设置)
- 点击"保存为预设"
- 命名
- 未来聊天时从下拉菜单选择
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 mode1. 开始聊天(无@提及)→ 默认工具可用
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 report1. 在工作流构建器中创建工作流:
输入 → 网页搜索 → 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 → reports1. 创建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 (not
create-table)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:migratetypescript
// 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:migrateServer Action Validators & Coding Standards
服务器操作验证器与编码标准
Server Action Validators (lib/action-utils.ts
)
lib/action-utils.ts服务器操作验证器(lib/action-utils.ts
)
lib/action-utils.tsCentralized 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
命名规范
| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | |
| Component files | kebab-case or PascalCase | |
| Hooks | camelCase with | |
| Utilities | camelCase | |
| API routes | Next.js convention | |
| Types | Domain suffix | |
| 类型 | 规范 | 示例 |
|---|---|---|
| 组件 | PascalCase | |
| 组件文件 | kebab-case 或 PascalCase | |
| 钩子 | camelCase 带 | |
| 工具库 | camelCase | |
| API路由 | Next.js约定 | |
| 类型 | 带领域后缀 | |
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 :
superRefinetypescript.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 :
superRefinetypescript.superRefine((data, ctx) => { if (data.password !== data.confirmPassword) { ctx.addIssue({ path: ["confirmPassword"], message: "Passwords must match" }) } })
Development Workflow
开发工作流
Core Commands
核心命令
bash
undefinedbash
undefinedDevelopment
开发
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
undefinedpnpm check # 运行代码检查 + 类型检查 + 测试
undefinedEnvironment Setup
环境设置
- Copy to
.env.example(auto-generated on.env)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: or
feat/feature-namefix/bug-name - Squash merge: Single commit per PR for clean history
- Main: 生产就绪代码
- 功能分支: 或
feat/feature-namefix/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
undefinedFirst-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 guidePrefixes: , , , , , , , ,
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:
- What changed
- Why it changed
- How you tested it
标题格式(Conventional Commits):
feat: 添加实时语音聊天
fix: 修复MCP工具流式错误
chore: 更新依赖
docs: 添加OAuth设置指南前缀: , , , , , , , ,
feat:fix:chore:docs:style:refactor:test:perf:build:需要可视化文档:
- UI变更的前后截图
- 交互功能的屏幕录制
- 响应式更新的移动端 + 桌面端视图
描述应说明:
- 变更内容
- 变更原因
- 测试方式
Pre-Submission Checklist
提交前检查清单
bash
undefinedbash
undefinedMust 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 or for server actions
✅ Check tool types with branded type tags before execution
✅ Use Zod for cross-field validation
✅ Add unit tests (happy path + one failure mode)
✅ Run before PR submission
✅ Include visual documentation for UI changes
✅ Use Conventional Commit format for PR titles
✅ Run E2E tests when touching critical flows
validatedActionWithUservalidatedActionWithAdminPermissionsuperRefinepnpm check✅ 服务器操作使用或
✅ 执行前使用品牌类型标签检查工具类型
✅ 跨字段校验使用Zod
✅ 添加单元测试(正常路径 + 一种失败场景)
✅ PR提交前运行
✅ UI变更包含可视化文档
✅ PR标题使用Conventional Commit格式
✅ 修改关键流程时运行E2E测试
validatedActionWithUservalidatedActionWithAdminPermissionsuperRefinepnpm checkNever 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
❌ Submit PR without visual docs (if UI change)
❌ Use non-conventional commit format
pnpm check❌ 服务器操作未使用认证验证器
❌ 未进行运行时检查就假设工具类型
❌ 手动解析FormData(使用验证器)
❌ 直接修改Zustand状态(使用浅更新)
❌ 在清空数据库时跳过首次用户测试
❌ 未运行就提交
❌ UI变更未包含可视化文档就提交PR
❌ 使用非Conventional Commit格式
pnpm checkKnown 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 or
validatedActionWithUservalidatedActionWithAdminPermissiontypescript
// ❌ 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
}
)错误: 未授权用户访问受保护操作
原因: 手动认证实现不一致
预防: 使用或
validatedActionWithUservalidatedActionWithAdminPermissiontypescript
// ❌ 错误:手动认证检查
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
superRefinetypescript
// ❌ 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
superRefinetypescript
// ❌ 错误:分开检查
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
undefinedpnpm test:e2e
undefinedIssue #7: Environment Config Mistakes
问题#7: 环境配置错误
Error: Missing required environment variables causing crashes
Why It Happens: Not copying to
Prevention: Auto-generated on
.env.example.env.envpnpm ibash
undefined错误: 缺少必填环境变量导致崩溃
原因: 未将复制到
预防: 时自动生成
.env.example.envpnpm i.envbash
undefinedAuto-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
undefinedundefinedIssue #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/)
- - Full repository guidelines (loaded when detailed structure questions arise)
references/AGENTS.md - - Complete contribution process (loaded when PR standards questions arise)
references/CONTRIBUTING.md
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 - - 完整贡献流程(当出现PR标准问题时加载)
references/CONTRIBUTING.md
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
官方文档
- better-chatbot: https://github.com/cgoinglove/better-chatbot
- Next.js: https://nextjs.org/docs
- Vercel AI SDK: https://sdk.vercel.ai/docs
- Better Auth: https://www.better-auth.com/docs
- Drizzle ORM: https://orm.drizzle.team/docs
- Playwright: https://playwright.dev/docs/intro
- Live Demo: https://betterchatbot.vercel.app
- better-chatbot: https://github.com/cgoinglove/better-chatbot
- Next.js: https://nextjs.org/docs
- Vercel AI SDK: https://sdk.vercel.ai/docs
- Better Auth: https://www.better-auth.com/docs
- Drizzle ORM: https://orm.drizzle.team/docs
- Playwright: https://playwright.dev/docs/intro
- 在线演示: https://betterchatbot.vercel.app
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 (auto-generates
pnpm i).env - Configure required env vars (DATABASE_URL, LLM_API_KEY)
- Run and verify it starts
pnpm dev - Create feature branch
- Add unit tests for new features
- Run before PR
pnpm check - Run if touching critical flows
pnpm test:e2e - Include visual docs (screenshots/recordings)
- Use Conventional Commit title
- Squash merge when approved
Questions? Issues?
- Check for detailed guidelines
references/AGENTS.md - Check for PR process
references/CONTRIBUTING.md - Check official docs: https://github.com/cgoinglove/better-chatbot
- 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
有疑问?遇到问题?
- 查看获取详细指南
references/AGENTS.md - 查看获取完整贡献流程
references/CONTRIBUTING.md - 查看官方文档: https://github.com/cgoinglove/better-chatbot
- 确保PostgreSQL和LLM API密钥已配置
Token效率: ~60% 节省 | 预防错误: 8个 | 生产验证: 是