关于 better-chatbot
这是一份面向 better-chatbot 平台开发者的项目规范指南,记录聊天、工具、工作流、数据库访问及 MCP 集成的架构和约定。
主要功能
- 01API 与组件规范
- 02三层工具架构
- 03MCP 客户端和工具管理模式
- 04工作流与 Agent 组合
- 05数据库仓库模式
- 06流式处理与容错
适用场景
- 01按项目约定实现聊天功能
- 02新增或调整 MCP 工具处理
- 03扩展工作流和内置工具
- 04修改数据库访问或结构
MCP 相关
指南介绍 better-chatbot 如何初始化已配置的 MCP 服务、收集并筛选其工具,再通过客户端管理器调用工具。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jackspace/ claudeskillz --skill better-chatbot安装后在 Claude Code 中输入/better-chatbot确认已加载。
附带文件README.mdSKILL.json
详细介绍
better-chatbot 贡献与规范技能
状态:已可投入生产 最后更新: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
概述
better-chatbot 是一个面向个人和团队的开源 AI 聊天机器人平台,基于 Next.js 15 和 Vercel AI SDK v5 构建。它将多模型 AI 支持(OpenAI、Anthropic、Google、xAI、Ollama、OpenRouter)与 MCP(Model Context Protocol)工具集成、可视化工作流构建器、实时语音助手和团队协作等高级功能相结合。
此技能会向 Claude 介绍 better-chatbot 特有的约定和模式,确保贡献符合既定标准并避免常见问题。
项目架构
目录结构
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
API 架构与设计模式
路由结构理念
约定:遵循 Next.js App Router 约定的 RESTful 资源
/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)
标准路由处理程序模式
位置:src/app/api/
模板结构:
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/chat/shared.chat.ts
此文件演示了如何处理:
- 工具加载(
loadMcpTools、loadWorkFlowTools、loadAppDefaultTools) - 筛选与组合(
filterMCPToolsByMentions、excludeToolExecution) - 系统提示词构建(
mergeSystemPrompt) - 手动工具执行处理
模式:
// 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
原因:遵循 DRY 原则,确保单一事实来源和行为一致
使用 safe() 进行防御式编程
库:使用 ts-safe 进行函数式错误处理
理念:绝不让聊天崩溃——平稳降级功能
// Returns empty object on failure, chat continues
const MCP_TOOLS = await safe()
.map(errorIf(() => !isToolCallAllowed && "Not allowed"))
.map(() => loadMcpTools({ mentions, allowedMcpServers }))
.orElse({}); // Graceful fallback
流式优先架构
模式:使用 Vercel AI SDK 流式处理工具
// 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 });
原因:提供实时反馈,改善 UX,并处理长时间运行的操作
工具系统深入解析
三层工具架构
设计目标:平衡可扩展性(MCP)、可组合性(工作流)和开箱即用的能力(默认工具)
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)
第 1 层:MCP 工具(外部集成)
位置:src/lib/ai/mcp/
理念:将 Model Context Protocol 服务作为一等工具
管理器模式:
// 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
为什么使用全局单例?
- Next.js 开发环境热重载 → 每次更改都重新连接 MCP 服务会产生高昂开销
- 在 HMR 更新期间保持状态
- 生产环境中只需要一个实例
工具封装:
// 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)
第 2 层:工作流工具(可视化组合)
位置:src/lib/ai/workflow/
理念:通过 @workflow_name 将可视化工作流转换为可调用工具
节点类型:
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
}
流式执行:
// 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 }
});
}
});
主要功能:在聊天 UI 中实时更新进度
第 3 层:默认工具(内置功能)
位置:src/lib/ai/tools/
类别:
export const APP_DEFAULT_TOOL_KIT = {
[AppDefaultToolkit.Visualization]: {
CreatePieChart, CreateBarChart, CreateLineChart,
CreateTable, CreateTimeline
},
[AppDefaultToolkit.WebSearch]: {
WebSearch, WebContent
},
[AppDefaultToolkit.Http]: {
Http
},
[AppDefaultToolkit.Code]: {
JavascriptExecution, PythonExecution
},
};
工具实现模式:
// 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} />;
}
为什么要分离?
- 服务端:纯数据/业务逻辑
- 客户端:丰富的可视化/交互
- 更易测试,性能更佳
工具生命周期
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
基于约定的扩展
添加新工具类型很简单:
- 在
AppDefaultToolkit中添加枚举 - 使用
createTool()实现工具 - 添加到
APP_DEFAULT_TOOL_KIT - 工具会自动通过
@toolname提供
组件与设计理念
按功能组织
位置:src/components/
components/
├── ui/ → shadcn/ui primitives
├── layouts/ → App structure
├── agent/ → Agent-specific
├── workflow/ → Workflow editor
├── tool-invocation/ → Tool result rendering
└── *.tsx → Shared components
原则:按功能分组,而非按类型分组
复合组件模式
示例:message.tsx + message-parts.tsx
理念:将复杂组件拆分为可组合的部分
// 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
});
}
客户端组件封装模式
示例:chat-bot.tsx
结构:
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} />
</>
);
}
原因:顶层组件负责协调,并将渲染工作委托给专用组件
工具结果渲染分离
关键架构决策:
- 工具执行位于
lib/ai/tools/ - 工具渲染位于
components/tool-invocation/
示例:
// 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} />;
}
优点:
- 职责分离明确
- 更便于测试
- 客户端可以功能丰富、交互性强,而不会增加服务端的复杂性
数据库与仓库模式
仓库模式架构
位置:src/lib/db/
结构:
db/
├── repository.ts → Single import point
├── pg/
│ ├── db.pg.ts → Drizzle connection
│ ├── schema.pg.ts → Table definitions
│ └── repositories/ → Feature queries
└── migrations/ → Drizzle migrations
理念:通过仓库接口对数据库进行抽象
接口优先设计
模式:
// 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;
原因:
- 易于替换实现(pg → sqlite)
- 无需数据库即可测试
- 整个代码库使用一致的 API
查询优化策略
1. 在外键上创建索引:
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. 选择性加载:
// Load minimal data
selectThread(id) → { id, title, userId, createdAt }
// Load full data when needed
selectThreadDetails(id) → { ...thread, messages, userPreferences }
3. SQL 聚合:
// 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. Modify schema in src/lib/db/pg/schema.pg.ts
export const NewTable = pgTable("new_table", { ... });
# 2. Generate migration
pnpm db:generate
# 3. Review generated SQL in drizzle/migrations/
# 4. Apply migration
pnpm db:migrate
# 5. Optional: Visual DB exploration
pnpm db:studio
架构原则
1. 渐进式增强
功能分层构建:
Base Layer: Chat + LLM
↓
Tool Layer: Default + MCP
↓
Composition Layer: Workflows (tools as nodes)
↓
Personalization Layer: Agents (workflows + prompts)
依据:
- Agent 可以拥有
instructions.mentions(注入工具/工作流) - 工作流可以调用 MCP 和默认工具
- 聊天 API 将这三个层级组合起来
用户路径:
- 从默认工具开始(无需设置)
- 为特定需求添加 MCP 服务
- 将其组合为工作流以实现自动化
- 封装为 Agent,定义角色
2. 约定优于配置
新增工具?
- 添加到
AppDefaultToolkit枚举中 → 自动可用
新增工作流节点?
- 添加到
NodeKind枚举中 → 执行器会处理它
新增 MCP 服务?
- 只需通过 UI 配置 → 管理器会处理其生命周期
3. 防御式编程
在所有地方使用 safe():
const tools = await safe(() => loadMcpTools())
.orElse({}); // Returns default on failure
理念:绝不让聊天崩溃——平稳降级
4. 流式优先
依据:
- 聊天 API 使用
createUIMessageStream() - 工作流执行会流式传输中间步骤
- 工具调用会流式传输进度更新
原因:实时反馈、提升用户体验,并能处理长时间运行的操作
5. 类型驱动开发
模式:
// 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)
扩展点参考
快速查找:“我想添加 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/schema.pg.ts + lib/db/pg/repositories/[name]-repository.pg.ts | 然后运行 pnpm db:generate 和 pnpm db:migrate |
| 新的 UI 组件 | 在领域文件夹中创建 | src/components/[domain]/[name].tsx | 使用 components/ui/ 中的 shadcn/ui 基元 |
| 新的 React Hook | 使用 use- 前缀创建 | src/hooks/use-[name].ts 或 src/hooks/queries/use-[name].ts | 数据获取 Hook 放在 queries/ 子文件夹中 |
| 新的 Zod 架构 | 添加到验证模块 | src/lib/validations/[domain].ts | 使用 z.infer<typeof schema> 定义 TypeScript 类型 |
| 新的 AI 提供商 | 添加到提供商注册表 | lib/ai/providers.ts | 使用 AI SDK 中的 createOpenAI、createAnthropic 等 |
| 新的 MCP 服务 | 通过 UI 配置 | 设置 → MCP Servers | 无需更改代码(使用文件或数据库存储) |
| 新的 Agent 模板 | 通过 UI 创建 | Agents 页面 | 组合工具/工作流/提示词 |
| 新的权限类型 | 添加到权限枚举 | lib/auth/permissions.ts | 在 validatedActionWithAdminPermission 中使用 |
| 新的 E2E 测试 | 添加测试文件 | tests/[feature].spec.ts | 使用 Playwright,并遵循现有模式 |
| 新的系统提示词 | 添加到提示词模块 | lib/ai/prompts.ts | 使用 mergeSystemPrompt 进行组合 |
常见开发流程
端到端添加功能:
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
端到端添加工具:
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
端到端添加工作流节点:
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
UX 模式与 @Mention 系统
@Mention 设计理念
核心设计原则:每项功能都可通过 @mentions 即时访问,无需翻找菜单。
重要性:用户无需切换上下文,即可即时组合功能。
三种 @Mention 类型
1. @tool(默认工具)
格式:@tool("tool_name")
示例:
@tool("web-search") find recent AI papers
@tool("create-table") show sales data
@tool("python-execution") calculate fibonacci
工作原理:
- 从服务端消息中解析
- 从
APP_DEFAULT_TOOL_KIT加载对应工具 - 大模型根据提示词决定何时调用
使用场景:内置功能(搜索、可视化、代码执行)
2. @mcp(MCP 服务工具)
格式:@mcp("server_name") 或指定工具 @mcp("server_name:tool_name")
示例:
@mcp("github") create an issue in my repo
@mcp("playwright") navigate to google.com
@mcp("slack:send-message") post update to #general
工作原理:
- 提及内容用于筛选要加载的 MCP 服务/工具
- 减少 Token 用量(仅向大模型发送相关工具)
- MCP 管理器负责连接和执行
使用场景:外部集成(GitHub、Slack、数据库等)
3. @workflow(自定义工作流)
格式:@workflow("workflow_name")
示例:
@workflow("customer-onboarding") process new signup
@workflow("data-pipeline") transform and analyze CSV
工作原理:
- 工作流会转换为可调用工具
- 大模型将工作流视为带有描述的单个工具
- 执行过程会流式传输中间节点结果
使用场景:多步骤自动化、业务流程
4. @agent(Agent 角色)
格式:从下拉菜单中选择 Agent(不在消息中输入)
工作原理:
- Agent 的
instructions.mentions会自动注入工具/工作流 - 系统提示词会添加到对话开头
- 预设可以覆盖模型/温度
使用场景:特定角色的上下文(编码助手、数据分析师等)
工具选择模式
上下文:用户从下拉菜单中选择模式
自动模式(默认)
- 大模型可以自主调用工具
- 每条消息可以调用多个工具
- 最适用于:自动化、工作流、探索
示例流程:
User: @tool("web-search") find AI news, then @tool("create-table") summarize
→ LLM searches → formats results → creates table → returns message
手动模式
- 大模型提出工具调用,等待用户批准
- 用户会看到带参数的“工具:web-search”,然后点击“执行”
- 最适用于:敏感操作、学习、调试
示例流程:
User: @mcp("github") create issue
→ LLM proposes: create_issue(repo="...", title="...", body="...")
→ User reviews and clicks "Execute"
→ Tool runs → result shown
无模式
- 不加载工具(纯文本对话)
- 降低延迟和令牌用量
- 最适用于:头脑风暴、解释、简单查询
预设系统
什么是预设?
- 针对常见场景的快速配置
- 按用户存储
- 可以覆盖:模型、温度、toolChoice、允许使用的 MCP 服务
使用场景示例:
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
创建方法:
- 配置聊天(模型、工具、设置)
- 点击“另存为预设”
- 为其命名
- 在后续聊天中从下拉菜单选择
用户使用流程示例
初级:首次使用
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. 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
高级: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. 易于发现
- 每个工具都应有清晰的描述(显示在大模型上下文中)
- 使用有意义的名称(
create-table,而不是tool-42)
2. 可组合性
- 工具应专注于单一用途
- 工作流组合工具
- Agent 组合工作流 + 工具 + 上下文
3. 渐进式展示
- 初级:自动模式,不使用 @提及(使用默认值)
- 中级:显式使用 @tool/@mcp 提及
- 高级:工作流、Agent、预设
4. 反馈
- 对于长时间运行的操作使用流式传输
- 为工作流提供进度更新
- 提供清晰且包含解决方案的错误消息
实用模板
模板:添加新的默认工具
// 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} />;
}
模板:添加新的 API 路由
// 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 });
}
}
模板:添加新的存储库
// 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
服务端操作验证器与编码规范
服务端操作验证器(lib/action-utils.ts)
用于经过验证、受权限控制的服务端操作的集中式模式:
// 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) => { ... })
可避免:
- 忘记进行身份验证检查 ✓
- 验证方式不一致 ✓
- FormData 解析错误 ✓
- 非标准错误响应 ✓
2. 工具抽象系统 使用带品牌标记的类型标签,为多种工具类型提供统一接口:
// 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
}
工具类型:
- MCP 工具:Model Context Protocol 集成
- 工作流工具:基于可视化 DAG 的工作流
- 默认工具:内置功能(搜索、代码执行等)
3. 工作流执行引擎 基于 DAG 的工作流系统,支持实时流式传输:
- 通过
dataStream.write()流式传输节点执行进度 - 跟踪:状态、输入/输出、错误、耗时
- 令牌优化:存储历史记录时不包含详细结果
4. 状态管理 使用浅比较的 Zustand 存储管理工作流和应用配置。
编码规范
命名约定
| 类型 | 约定 | 示例 |
|---|---|---|
| 组件 | PascalCase | ChatBot.tsx, WorkflowBuilder.tsx |
| 组件文件 | kebab-case 或 PascalCase | chat-bot.tsx, ChatBot.tsx |
| Hooks | 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 规范
- 全面使用 严格 TypeScript(不使用隐式 any)
- 使用 Zod 进行验证和类型推断:
const schema = z.object({ name: z.string() }) type SchemaType = z.infer<typeof schema> - 使用 自定义类型标签 进行运行时类型收窄(参见工具抽象)
- 在
src/types/中按领域组织 类型
代码质量
- 行宽:80 个字符
- 缩进:2 个空格
- 格式化工具:Biome 1.9.4
- 代码检查工具:Biome(不使用 ESLint)
- 验证:所有场景均使用 Zod(表单、API、动态配置)
错误处理
- 针对特定错误使用 枚举错误类型:
enum UpdateUserPasswordError { INVALID_CURRENT_PASSWORD = "invalid_current_password", PASSWORD_MISMATCH = "password_mismatch" } - 使用 Zod
superRefine进行 跨字段验证:.superRefine((data, ctx) => { if (data.password !== data.confirmPassword) { ctx.addIssue({ path: ["confirmPassword"], message: "Passwords must match" }) } })
开发工作流
核心命令
# Development
pnpm dev # Start dev server
pnpm build # Production build
pnpm start # Start production server
pnpm lint:fix # Auto-fix linting issues
# Database (Drizzle ORM)
pnpm db:generate # Generate migrations
pnpm db:migrate # Run migrations
pnpm db:push # Push schema changes
pnpm db:studio # Open 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
# Quality Check
pnpm check # Run lint + type-check + tests
环境设置
- 将
.env.example复制为.env(运行pnpm i时会自动生成) - 必需:PostgreSQL 连接,以及至少一个 LLM API 密钥
- 可选:OAuth 提供商(Google、GitHub、Microsoft)、Redis、Vercel Blob
分支策略
- 主分支:可用于生产的代码
- 功能分支:
feat/feature-name或fix/bug-name - 压缩合并:每个 PR 合并为一个提交,保持提交历史整洁
测试模式
单元测试(Vitest)
- 与源代码 放在一起(
*.test.ts) - 覆盖范围:至少覆盖成功路径和一种失败情况
- 示例:
// 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 测试(Playwright)
多用户场景和首位用户场景采用 专门的编排方式:
# First-user tests (clean DB → signup → verify admin role)
pnpm test:e2e:first-user
# Standard tests (assumes first user exists)
pnpm test:e2e:standard
# Full suite (first-user → standard)
pnpm test:e2e
测试项目依赖关系确保按顺序执行:
- 清理数据库
- 执行首位用户注册和角色验证
- 执行标准多用户测试
测试运行之间共享身份验证状态,避免重新登录。
使用 种子/清理脚本确保测试结果确定。
贡献指南
开始之前
重大更改需先讨论:
- 新 UI 组件
- 新 API 端点
- 外部服务集成
- 破坏性更改
无需事先批准:
- Bug 修复
- 文档改进
- 小幅重构
Pull Request 规范
标题格式(Conventional Commits):
feat: Add realtime voice chat
fix: Resolve MCP tool streaming error
chore: Update dependencies
docs: Add OAuth setup guide
前缀:feat:、fix:、chore:、docs:、style:、refactor:、test:、perf:、build:
必须提供可视化文档:
- UI 更改需提供更改前后的截图
- 交互功能需提供屏幕录制
- 响应式更新需提供移动端和桌面端视图
描述应说明:
- 更改内容
- 更改原因
- 测试方式
提交前检查清单
# Must pass before PR:
pnpm check # Lint + type-check + tests
pnpm test:e2e # E2E tests (if applicable)
- 为新功能/Bug 修复添加测试
- 附上可视化文档(如有 UI 更改)
- 标题符合 Conventional Commits 规范
- 描述说明更改内容、原因和测试方式
关键规则
始终执行
✅ 使用 validatedActionWithUser 或 validatedActionWithAdminPermission 编写服务端操作
✅ 执行前通过品牌类型标签检查工具类型
✅ 使用 Zod superRefine 进行跨字段验证
✅ 添加单元测试(成功路径和一种失败情况)
✅ 提交 PR 前运行 pnpm check
✅ 为 UI 更改附上可视化文档
✅ PR 标题使用 Conventional Commit 格式
✅ 修改关键流程时运行 E2E 测试
绝不执行
❌ 不使用身份验证验证器就实现服务端操作
❌ 不进行运行时检查就假定工具类型
❌ 手动解析 FormData(应使用验证器)
❌ 直接修改 Zustand 状态(应使用浅层更新)
❌ 在干净的数据库上跳过首位用户测试
❌ 不运行 pnpm check 就提交
❌ UI 有更改却未附上可视化文档就提交 PR
❌ 使用非 Conventional Commit 格式
已知问题预防
此技能可预防 8 个已记录的问题:
问题 #1:忘记在服务端操作中进行身份验证
错误:未经授权的用户访问受保护的操作
原因:手动实现身份验证时容易出现不一致
预防方法:使用 validatedActionWithUser 或 validatedActionWithAdminPermission
// ❌ 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
}
)
问题 #2:工具类型不匹配
错误:执行工具时发生运行时类型错误 原因:执行前未检查工具类型 预防方法:使用品牌类型标签进行运行时类型收窄
// ❌ 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)
}
问题 #3:FormData 解析错误
错误:表单提交的错误处理不一致 原因:手动解析 FormData 并临时编写验证逻辑 预防方法:由验证器自动处理解析
// ❌ 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
})
问题 #4:跨字段验证问题
错误:密码不匹配验证不起作用
发生原因:对相关字段分别进行验证
预防方法:使用 Zod superRefine
// ❌ 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"
})
}
})
问题 #5:工作流状态不一致
错误:Zustand 状态更新未触发重新渲染 发生原因:深层修改嵌套的工作流状态 预防方法:使用浅层更新
// ❌ 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
)
}
}))
问题 #6:缺少 E2E 测试设置
错误:在干净的数据库上运行 E2E 测试失败 发生原因:首次用户设置之前运行标准测试 预防方法:使用正确的测试命令
# ❌ BAD: Running standard tests on clean DB
pnpm test:e2e:standard
# ✅ GOOD: Full suite with first-user setup
pnpm test:e2e
问题 #7:环境配置错误
错误:缺少必需的环境变量导致崩溃
发生原因:未将 .env.example 复制为 .env
预防方法:在 pnpm i 时自动生成 .env
# Auto-generates .env on install
pnpm i
# Verify all required vars present
# Required: DATABASE_URL, at least one LLM_API_KEY
问题 #8:提交消息格式不正确
错误:未遵循约定式提交格式导致 CI/CD 失败 发生原因:未遵循 Conventional Commits 标准 预防方法:使用“前缀 + 冒号”格式
# ❌ BAD:
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"
常见模式
模式 1:带用户上下文的服务器操作
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 }
}
)
适用场景:任何需要身份验证的服务器操作
模式 2:工具类型检查
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)
}
}
适用场景:在统一界面中处理多种工具类型
模式 3:工作流状态更新
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
)
}
}))
适用场景:在不修改状态的情况下更新嵌套的 Zustand 状态
使用捆绑资源
参考资料(references/)
references/AGENTS.md- 完整的仓库指南(出现详细结构相关问题时加载)references/CONTRIBUTING.md- 完整的贡献流程(出现 PR 标准相关问题时加载)
Claude 应在以下情况下加载这些资料:用户询问 better-chatbot 的详细约定、询问“完整指南是什么”,或需要全面了解贡献工作流。
依赖项
必需:
- [email protected] - 框架
- [email protected] - Vercel AI SDK
- [email protected] - 身份验证
- [email protected] - 数据库 ORM
- @modelcontextprotocol/[email protected] - MCP 支持
- [email protected] - 验证
- [email protected] - 状态管理
测试:
- [email protected] - 单元测试
- @playwright/[email protected] - E2E 测试
官方文档
- 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
生产环境示例
此技能基于 better-chatbot 的生产标准:
- 在线运行:https://betterchatbot.vercel.app
- 测试:48+ 个 E2E 测试通过
- 错误:0(已预防全部 8 个已知问题)
- 验证:✅ 多用户场景、工作流执行、MCP 工具
完整设置清单
为 better-chatbot 做贡献时:
- Fork 并克隆仓库
- 运行
pnpm i(自动生成.env) - 配置必需的环境变量(DATABASE_URL、LLM_API_KEY)
- 运行
pnpm dev并确认其正常启动 - 创建功能分支
- 为新功能添加单元测试
- 提交 PR 前运行
pnpm check - 如果涉及关键流程,运行
pnpm test:e2e - 附上可视化文档(截图/录屏)
- 使用 Conventional Commit 标题
- 审批通过后采用 Squash 合并
有疑问或遇到问题?
- 查看
references/AGENTS.md,了解详细指南 - 查看
references/CONTRIBUTING.md,了解 PR 流程 - 查看官方文档:https://github.com/cgoinglove/better-chatbot
- 确认已配置 PostgreSQL 和 LLM API 密钥
Token 效率:节省约 60% | 预防错误数:8 | 已通过生产验证:是