# nfap9 个人知识库全文聚合 > 生成时间:2026-06-30T02:26:47.904Z > 站点:https://nfap9.github.io > 本文档包含所有博客文章和笔记的完整 Markdown 内容,供 LLM 检索和分析使用。 > 文章总数:16(博客 14 篇 + 笔记 2 条) --- # 用 Node.js 从零构建一个 Agent 运行时:基于 aura-agent 的架构拆解 > **类型**:博客 > **URL**:https://nfap9.github.io/blog/nodejs-agent-architecture/ > **分类**:工程化 > **标签**:nodejs, agent, ai, mcp, react, architecture > **发布日期**:2026-06-18 > **描述**:基于真实开源项目 aura-agent,拆解 Agent 的六大核心要素:ReAct 循环、工具系统、记忆系统、MCP 协议、技能注入、流式传输。不讲故事,直接看代码。 > 本文基于开源项目 [aura-agent](https://github.com/nfap9/aura-agent) 的真实代码,逐层拆解一个 Node.js Agent 运行时的实现。所有代码片段均来自项目源码,不做虚构。 --- ## 一、Agent 的本质:一个编排循环 Agent 不是魔法,它就是一个**带状态的循环**。 ``` 接收输入 → 注入上下文 → 调用 LLM → 解析响应 → 执行工具 → 把结果喂回去 → 再次调用 LLM ``` 这个循环在 aura-agent 中叫做 `runLoop`,最大迭代 10 次: ```typescript // packages/core/src/agent/agent.ts const MAX_ITERATIONS = 10; private async *runLoop( inputMessages: Message[], options?: ChatCompletionOptions, events?: AgentEvents ): AsyncGenerator { // 1. 注入记忆上下文 if (this.memory && inputMessages.length > 0) { const memoryContext = await this.memory.getRelevantContext(query); // 把记忆塞进第一条用户消息前面 } this.thread.addMessages(inputMessages); this.thread.trim(); // 截断超长的上下文 let continueLoop = true; let iteration = 0; while (continueLoop && iteration < MAX_ITERATIONS) { iteration++; const messages = this.thread.getMessages(); // 2. 匹配技能并注入 const apiMessages = await this.buildMessagesWithSkills(userQuery, events); // 3. 调用 LLM(流式) const stream = this.provider.chatStream({ model: this.model, messages: apiMessages, tools: this.tools.getToolSchemas(), }); let content = ""; let hasToolCall = false; for await (const chunk of stream) { if (chunk.type === "content") { content += chunk.delta; yield chunk; // 流式吐给外层 } else if (chunk.type === "tool_call") { hasToolCall = true; // 收集工具调用参数 } } // 4. 把 assistant 的回复加入对话历史 this.thread.addMessages([assistantMessage]); if (!hasToolCall) { continueLoop = false; // 没有工具调用,任务结束 } else { // 5. 执行工具,把结果以 tool 角色塞回历史 for (const toolCall of toolCalls) { const result = await this.tools.execute(name, parseArgs); this.thread.addMessages([ { role: "tool", content: result, tool_call_id: toolCall.id } ]); } // 循环继续,LLM 看到工具结果后再次推理 } } } ``` 这就是 ReAct 的核心:**LLM 不直接给答案,而是输出 "我要调用某某工具",Agent 执行工具后把结果还回去,LLM 基于结果再推理下一步**。一轮不够就再来一轮,直到 LLM 说"不用工具了,这就是答案"。 --- ## 二、工具系统:让 LLM 能动手 ### 2.1 工具的组成 一个工具 = 定义(告诉 LLM 有什么参数)+ 处理器(实际执行)。 ```typescript // packages/core/src/capabilities/tools/types.ts export interface ToolDefinition { name: string; description: string; parameters: { type: string; properties: Record; required?: string[]; }; } export type ToolHandler = (args: Record) => Promise | string; ``` 注册时把两者绑定在一起: ```typescript // packages/core/src/capabilities/tools/registry.ts export class ToolRegistry implements ToolSource { private tools = new Map(); register(definition: ToolDefinition, handler: ToolHandler): void { this.tools.set(definition.name, { definition, handler }); } getToolSchemas(): ChatCompletionTool[] { return Array.from(this.tools.values()).map((t) => ({ type: "function", function: { name: t.definition.name, description: t.definition.description, parameters: t.definition.parameters, }, })); } async execute(name: string, args: Record): Promise { const tool = this.tools.get(name); return await tool.handler(args); } } ``` `getToolSchemas()` 返回的格式就是 OpenAI 的 `tools` 参数,LLM 收到后就知道自己有哪些工具可用。 ### 2.2 内置工具示例:bash ```typescript // packages/core/src/capabilities/tools/native/bash.ts export const bashDefinition: ToolDefinition = { name: "bash", description: "执行 shell 命令...", parameters: { type: "object", properties: { command: { type: "string", description: "要执行的 shell 命令" }, timeout: { type: "number", description: "超时时间(毫秒)" }, }, required: ["command"], }, }; export async function executeBash(args: Record): Promise { const { command, timeout = 60000 } = args; const { stdout, stderr } = await execAsync(command, { timeout, maxBuffer: 1024 * 1024 }); return stdout || "(命令执行完成,无输出)"; } ``` 注册时调用: ```typescript registry.register(bashDefinition, (args) => executeBash(args)); ``` --- ## 三、MCP:连接外部工具生态 MCP(Model Context Protocol)是 Anthropic 推出的标准协议,让外部工具服务器和 Agent 之间用统一格式通信。aura-agent 的 MCP 实现支持 stdio 和 HTTP 两种传输方式。 ### 3.1 连接 MCP 服务器 ```typescript // packages/core/src/capabilities/tools/mcp.ts export class MCPClientManager { private connections: Map = new Map(); async connect(config: MCPConfig): Promise { for (const serverConfig of config.servers) { await this.connectServer(serverConfig); } } private async connectServer(config: MCPServerConfig): Promise { const client = new Client({ name: "agent-mcp-client", version: "1.0.0" }, { capabilities: {} }); let transport: Transport; if (config.transport === "stdio") { transport = new StdioClientTransport({ command: config.command, args: config.args, }); } else { transport = new StreamableHTTPClientTransport(new URL(config.url!)); } await client.connect(transport); // 获取服务器暴露的工具列表 const toolsResult = await client.listTools(); const tools: ToolDefinition[] = toolsResult.tools.map((t) => ({ name: t.name, description: t.description ?? "", parameters: { type: "object", properties: t.inputSchema.properties ?? {}, required: t.inputSchema.required ?? [], }, })); this.connections.set(config.name, { config, client, transport, tools }); } } ``` ### 3.2 命名空间隔离 不同 MCP 服务器可能定义同名工具(比如两个服务器都有 `read_file`),所以 aura-agent 给工具名加上服务器前缀: ```typescript private makeUniqueToolName(serverName: string, toolName: string): string { return `${serverName}_${toolName}`; // e.g. "filesystem_read_file" } ``` LLM 看到的是 `filesystem_read_file`,调用时由 `MCPClientManager` 解析出原始服务器名和工具名,转发给对应的服务器。 ### 3.3 执行与结果转换 MCP 工具返回的结果格式多样(text / resource / image),aura-agent 统一转成字符串: ```typescript private async executeTool(uniqueName: string, args: Record): Promise { const result = await conn.client.callTool({ name: originalToolName, arguments: args }); if ("content" in result && Array.isArray(result.content)) { const texts = result.content .filter((c: any) => c.type === "text") .map((c: any) => c.text) .join("\n"); if (texts) return texts; const images = result.content .filter((c: any) => c.type === "image") .map((c: any) => `[image data: ${c.mimeType}]`) .join("\n"); if (images) return images; } return JSON.stringify(result); } ``` 配置一个 MCP 服务器只需要一个 JSON 文件: ```json { "servers": [ { "name": "filesystem", "transport": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/data"] } ] } ``` --- ## 四、记忆系统:让 Agent 有长期记忆 没有记忆的 Agent 每次对话都是白板。aura-agent 的记忆系统基于 **TF-IDF + 余弦相似度** 做检索,纯算法实现,不依赖向量数据库。 ### 4.1 记忆的数据结构 ```typescript // packages/core/src/capabilities/memory/types.ts export interface MemoryEntry { id: string; content: string; category: string; // 分类,如 "fact", "preference" importance: number; // 1-10,重要性评分 createdAt: string; updatedAt: string; accessCount: number; // 被检索次数 metadata?: Record; } ``` ### 4.2 检索:TF-IDF + 余弦相似度 ```typescript // packages/core/src/capabilities/memory/retriever.ts export function retrieveMemories( query: string, entries: MemoryEntry[], limit: number = 5, minScore: number = 0.05 ): MemoryResult[] { const queryTokens = tokenize(query); // 分词 const queryTf = termFrequency(queryTokens); // 词频 // 预计算所有文档的 TF const docTfs = entries.map((e) => termFrequency(tokenize(e.content))); // 计算 IDF(逆文档频率) const idf = computeIdf(docTfs); // 加权查询向量 const weightedQuery = applyIdf(queryTf, idf); for (let i = 0; i < entries.length; i++) { const weightedDoc = applyIdf(docTfs[i]!, idf); let score = cosineSimilarity(weightedQuery, weightedDoc); // 时间衰减:越老的记忆权重越低(半衰期 30 天) score *= timeDecay(entry.createdAt); // 重要性加权 score *= 0.5 + entry.importance / 10; // 访问频率加权 score *= 1 + Math.log1p(entry.accessCount) * 0.1; if (score >= minScore) results.push({ entry, score }); } return results.sort((a, b) => b.score - a.score).slice(0, limit); } ``` 分词策略:英文按单词分,中文按字分。这样实现简单,在中小规模记忆库(几百条)上效果足够好。 ### 4.3 自动合并与智能清理 保存记忆时,如果检测到相似度 > 0.8 的已有记忆,直接合并更新而不是新增: ```typescript async saveMemory(content: string, category = "fact", importance = 5): Promise { const similar = retrieveMemories(content, this.entries, 1, 0.8); if (similar.length > 0) { const existing = similar[0]!.entry; existing.content = this.mergeContent(existing.content, content); existing.updatedAt = now; existing.importance = Math.max(existing.importance, importance); existing.accessCount++; await this.persist(); return existing; } // ... 创建新记忆 } ``` 当记忆超过 500 条时,触发清理。评分公式: ``` score = 新鲜度(30%) + 重要性(40%) + 访问频率(30%) ``` ```typescript async cleanup(targetCount?: number): Promise { const scored = this.entries.map((entry) => { const daysOld = (now - new Date(entry.createdAt).getTime()) / (1000 * 60 * 60 * 24); const recencyScore = Math.exp(-daysOld / 30); // 指数衰减 const importanceScore = entry.importance / 10; const accessScore = Math.log1p(entry.accessCount) / Math.log1p(100); const totalScore = recencyScore * 0.3 + importanceScore * 0.4 + accessScore * 0.3; return { entry, score: totalScore }; }); scored.sort((a, b) => b.score - a.score); this.entries = scored.slice(0, target).map((s) => s.entry); } ``` 存储层可插拔:默认 `InMemoryStorage`,生产环境用 `FileMemoryStorage` 持久化到 JSON 文件。 ### 4.4 记忆如何注入对话 Agent 在每次循环开始前,用用户查询去检索记忆,把结果塞进第一条用户消息前面: ```typescript const memoryContext = await this.memory.getRelevantContext(query); // 结果格式: // ## 相关历史记忆 // 1. [fact] 用户喜欢使用 Vue 3 Composition API // 2. [preference] 项目使用 pnpm 管理依赖 ``` 这样 LLM 在推理时自然能看到相关背景,无需额外机制。 --- ## 五、技能系统:动态注入领域知识 Skill 是从 `SKILL.md` 文件加载的领域知识片段,运行时根据用户查询语义匹配,注入到对话上下文中。 ### 5.1 Skill 文件格式 ```markdown --- name: web-search description: 网络搜索技能,帮助用户查找最新信息 --- ## 使用场景 当用户询问实时信息、新闻、股价等需要联网查询的内容时,主动调用搜索工具。 ## 搜索策略 1. 提取关键词 2. 使用 search_web 工具 3. 综合结果回答用户 ``` ### 5.2 匹配机制 基于关键词匹配,简单但有效: ```typescript // packages/core/src/capabilities/skills/registry.ts match(query: string): Skill[] { const lowerQuery = query.toLowerCase(); const keywords = lowerQuery.split(/\s+/).filter((w) => w.length >= 1 && !isStopWord(w)); for (const skill of this.skills.values()) { if (skill.metadata.hidden === true) continue; const haystack = `${skill.name} ${skill.description} ${skill.content}`.toLowerCase(); let score = 0; for (const kw of keywords) { if (haystack.includes(kw)) score += 1; } if (score > 0) scored.push({ skill, score }); } scored.sort((a, b) => b.score - a.score); return scored.slice(0, this.maxActiveSkills).map((s) => s.skill); // 默认最多 3 个 } ``` 匹配到的 Skill 会作为一条 `system` 消息注入: ```typescript const skillMsg: Message = { role: "system", content: `[Skills]\n## web-search\n网络搜索技能...\n\n## 使用场景\n...`, }; ``` 放在已有 system prompt 之后,这样 LLM 既能看到全局指令,也能看到当前场景下的特定技能指导。 --- ## 六、流式传输:实时响应 ### 6.1 LLM 层:流式调用 ```typescript // packages/core/src/llm/openai.ts async *chatStream(params: { ... }): AsyncGenerator { const requestOptions = { model: params.model, messages: params.messages, tools: params.tools, tool_choice: params.options?.tool_choice ?? "auto", stream: true, // 关键 }; const stream = await this.client.chat.completions.create(requestOptions); for await (const chunk of stream) { const delta = chunk.choices[0]?.delta; if (delta?.content) { yield { type: "content", delta: delta.content }; } if (delta?.reasoning_content) { yield { type: "reasoning", delta: delta.reasoning_content }; } if (delta?.tool_calls) { for (const tc of delta.tool_calls) { yield { type: "tool_call", delta: "", toolCall: { index: tc.index ?? 0, id: tc.id, name: tc.function?.name, arguments: tc.function?.arguments }, }; } } } } ``` ### 6.2 Agent 层:逐块转发 Agent 不缓存完整内容,而是把流式块逐块 yield 出去: ```typescript for await (const chunk of stream) { if (chunk.type === "content") { content += chunk.delta; events?.onContentChunk?.(chunk.delta); // 触发外部事件 yield chunk; // 继续流给更外层 } else if (chunk.type === "tool_call") { hasToolCall = true; // 累加工具调用参数(流式拼接) yield chunk; } } ``` ### 6.3 CLI 层:实时展示 ```typescript // apps/cli/src/interfaces/cli/index.ts for await (const chunk of chat.chatStream(userInput, preset, events)) { if (chunk.type === "reasoning") { io.write(`${C.gray} 💭 `); // 灰色思考前缀 io.write(chunk.delta); } else if (chunk.type === "content") { io.write(`${C.brightCyan}● AI:${C.reset} `); io.write(chunk.delta); // 逐字输出 } else if (chunk.type === "tool_call") { io.output(`${C.gray} 🔧 准备调用工具...${C.reset}`); } } ``` 三层流式:`OpenAI SDK` → `Provider` → `Agent` → `CLI`,每一层都是 `AsyncGenerator`,没有中间缓存,用户感受到的是逐字输出。 --- ## 七、Thread:对话历史与上下文截断 `Thread` 只负责一件事:管理消息列表和 Token 估算。 ```typescript // packages/core/src/agent/thread.ts export class Thread { private messages: Message[] = []; private maxContextTokens: number; addSystemPrompt(content: string): void { ... } addMessages(messages: Message[]): void { ... } getMessages(): Message[] { ... } trim(): void { ... } // 截断超长上下文 } ``` Token 估算采用混合策略:有精确 tokenizer 就用,没有则按字符数估算(中文字符算 1 token,其他字符算 0.25 token)。 截断策略:保留 system prompt,从最早的消息开始删,但至少保留最近 2 条。这样避免上下文爆炸导致 API 费用飙升。 ```typescript trim(): void { let totalTokens = 0; for (const msg of this.messages) { totalTokens += this.estimateMessageTokens(msg); } if (totalTokens <= this.maxContextTokens) return; const startIndex = this.messages[0]?.role === "system" ? 1 : 0; const minKeepCount = 2; while (totalTokens > this.maxContextTokens && this.messages.length - startIndex > minKeepCount) { const removed = this.messages[startIndex]!; totalTokens -= this.estimateMessageTokens(removed); this.messages.splice(startIndex, 1); // 从头部删除 } } ``` --- ## 八、架构总览 ``` CLI 交互层 └─ chatStream() → 消费 StreamChunk,实时渲染 Agent 编排层 ├─ runLoop() → ReAct 循环,最多 10 轮 ├─ buildMessagesWithSkills() → 动态注入技能 ├─ Thread → 消息历史 + Token 截断 └─ MemorySource → 检索相关记忆注入上下文 能力层 ├─ ToolRegistry → 本地工具注册与执行 ├─ MCPClientManager → 外部 MCP 服务器连接 ├─ MemoryManager → TF-IDF 检索 + 自动合并/清理 └─ SkillRegistry → 关键词匹配 + 动态注入 模型层 └─ Provider → OpenAI / Anthropic / Gemini 统一抽象 ``` --- ## 九、关键设计决策 | 决策 | 理由 | |------|------| | TF-IDF 而非向量数据库 | 无需外部依赖,几百条记忆上效果足够,代码自包含 | | 纯 AsyncGenerator 流式 | 无中间缓存,内存友好,三层直通 | | 工具名加服务器前缀 | 避免 MCP 工具命名冲突,逻辑清晰 | | 记忆合并阈值 0.8 | 平衡去重与保留细节,避免记忆膨胀 | | Skill 关键词匹配 | 实现简单,运行时零依赖,效果在多数场景够用 | | Thread 头部截断 | 保留 system + 最近消息,丢失的是 oldest 而非 newest | | 最大 10 轮迭代 | 防止无限循环,同时覆盖绝大多数多步任务 | --- ## 十、如何运行 ```bash git clone https://github.com/nfap9/aura-agent.git cd aura-agent npm install # 配置环境变量 cp .env.example .env # 编辑 .env 填入 API_KEY 和 BASE_URL npm run dev:cli ``` 输入问题,看 Agent 如何一步步思考、调用工具、返回结果。 --- > 本文所有代码均来自 [aura-agent](https://github.com/nfap9/aura-agent) 仓库,可直接对照阅读。Agent 的实现不需要框架,理解循环、工具、记忆、流式这几个核心概念后,用几十行代码就能搭出一个最小可用版本。 --- # Docker 与 Docker Compose 实战指南 > **类型**:博客 > **URL**:https://nfap9.github.io/blog/docker-compose-guide/ > **分类**:工程化 > **标签**:docker, docker-compose, 容器化, 部署, 运维, 工程化 > **发布日期**:2026-06-17 > **更新日期**:2026-06-17 > **描述**:以 tenant-hub 项目为例,讲解 Docker 容器化与 Docker Compose 多容器编排的实际应用 > 本文以 [tenant-hub](https://github.com/nfap9/tenant-hub) 项目(公寓物业管理系统)为例,讲解 Docker 和 Docker Compose 的实际使用。该项目使用 pnpm monorepo 架构,包含后端 API、前端 Web 和小程序三个应用。 --- ## 核心概念 **镜像(Image)** 镜像是一个只读模板,包含运行应用所需的完整文件系统、依赖库和配置。基于 Dockerfile 构建生成。每一层构建指令创建一个新层,Docker 通过层缓存加速重复构建。 **容器(Container)** 容器是镜像的运行实例。拥有独立的进程空间、文件系统和网络,但共享宿主机内核。相比虚拟机,容器启动更快、资源占用更少。 **卷(Volume)** 容器停止后数据会丢失。卷用于将容器内的数据持久化到宿主机,或者在容器之间共享数据。 --- ## 安装配置 ### Ubuntu / Debian ```bash sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg lsb-release sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ``` ### macOS ```bash brew install --cask docker ``` 安装完启动 Docker Desktop 应用,确保 daemon 在运行。 ### 验证 ```bash docker --version docker compose version ``` ### 免 sudo 使用 ```bash sudo usermod -aG docker $USER newgrp docker ``` `newgrp docker` 使当前终端立即生效,其他终端需重新登录。 --- ## Docker 核心命令 ### 镜像操作 ```bash docker search nginx # 搜索镜像 docker pull nginx:latest # 拉取镜像 docker images # 列出本地镜像 docker rmi nginx:latest # 删除镜像 docker history nginx:latest # 查看构建层 ``` ### 容器操作 ```bash # 前台运行 docker run -it nginx # 后台运行,端口映射 docker run -d -p 8080:80 --name my-nginx nginx # 参数说明 # -it 交互式 + 伪终端 # -d 后台运行 # -p 8080:80 端口映射(宿主机:容器) # -v 卷挂载 # --name 指定容器名 # --rm 停止后自动删除 # -e 环境变量 docker ps # 查看运行中容器 docker ps -a # 查看所有容器 docker stop my-nginx # 停止 docker start my-nginx # 启动 docker restart my-nginx # 重含 docker logs -f my-nginx # 查看日志 docker exec -it my-nginx /bin/bash # 进入容器 docker rm my-nginx # 删除 docker rm -f my-nginx # 强制删除运行中容器 ``` ### 卷管理 ```bash docker volume create my-data # 创建命名卷 docker volume ls # 列表 docker run -d -v my-data:/data nginx # 挂载卷 docker run -d -v $(pwd)/data:/app/data nginx # 绑定挂载 docker volume prune # 清理未使用卷 ``` 绑定挂载将宿主机目录直接映射到容器,开发时常用。生产环境建议使用 Docker 管理的命名卷,避免宿主机与容器权限不一致的问题。 ### 网络管理 ```bash docker network ls # 列表 docker network create my-network # 创建 docker run -d --network my-network --name web nginx # 指定网络 docker network inspect my-network # 详情 ``` 自定义网络中,容器通过服务名互访,无需记录 IP。 --- ## Dockerfile 编写 以 tenant-hub 项目的后端 API 为例。该项目使用 pnpm monorepo,需要在工作空间中管理多个子项目的依赖。 ### 后端 API Dockerfile ```dockerfile FROM node:22-alpine AS base ENV PNPM_HOME=/pnpm ENV PATH=$PNPM_HOME:$PATH RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.aliyun.com|g' /etc/apk/repositories \ && corepack enable \ && apk add --no-cache openssl wget WORKDIR /app FROM base AS deps ENV HUSKY=0 COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./ COPY apps/api/package.json apps/api/package.json RUN pnpm install --frozen-lockfile FROM deps AS development COPY apps/api apps/api EXPOSE 4000 CMD ["sh", "-c", "pnpm --filter @tenant-hub/api prisma generate && pnpm --filter @tenant-hub/api dev"] FROM deps AS build COPY apps/api apps/api RUN pnpm --filter @tenant-hub/api prisma generate RUN pnpm --filter @tenant-hub/api build FROM base AS production ENV HUSKY=0 COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./ COPY apps/api/package.json apps/api/package.json COPY apps/api/prisma apps/api/prisma RUN pnpm install --frozen-lockfile --prod RUN pnpm --filter @tenant-hub/api prisma generate COPY --from=build /app/apps/api/dist apps/api/dist WORKDIR /app/apps/api EXPOSE 4000 CMD ["sh", "-c", "pnpm prisma migrate deploy && node dist/server.js"] ``` ### 关键细节 **多阶段构建** 该 Dockerfile 定义了 5 个阶段: 1. **base** —— 基础镜像,设置 pnpm 环境和安装系统依赖(openssl、wget) 2. **deps** —— 安装生产依赖,利用缓存加速 3. **development** —— 开发环境,包含 Prisma generate 和 dev 命令 4. **build** —— 构建产物 5. **production** —— 生产环境,仅包含构建产物和生产依赖,体积最小 **层缓存优化** 先复制依赖文件(`package.json`、`pnpm-lock.yaml`),再复制代码。这样代码修改时,依赖层缓存不会失效,构建速度大幅提升。 **生产环境启动** ``` CMD ["sh", "-c", "pnpm prisma migrate deploy && node dist/server.js"] ``` 生产环境启动时先执行数据库迁移,确保数据库 schema 与代码一致,然后启动应用。 ### 前端 Web Dockerfile ```dockerfile FROM node:22-alpine AS base ENV PNPM_HOME=/pnpm ENV PATH=$PNPM_HOME:$PATH RUN corepack enable WORKDIR /app FROM base AS deps COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./ COPY apps/tenant-web/package.json apps/tenant-web/package.json RUN pnpm install --frozen-lockfile FROM deps AS development COPY apps/tenant-web apps/tenant-web EXPOSE 5173 CMD ["pnpm", "--filter", "@tenant-hub/tenant-web", "dev"] FROM deps AS build ARG VITE_API_BASE_URL=http://localhost:4000/api ENV VITE_API_BASE_URL=$VITE_API_BASE_URL COPY apps/tenant-web apps/tenant-web RUN pnpm --filter @tenant-hub/tenant-web build FROM nginx:1.27-alpine AS production COPY apps/tenant-web/nginx.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/apps/tenant-web/dist /usr/share/nginx/html EXPOSE 80 ``` 前端构建产物是静态文件,最终用 Nginx 作为静态服务器。构建时通过 `ARG` 传入 API 基础地址,生成环境相关的静态资源。 --- ## Docker Compose 多容器编排 ### 基础结构 以下是 tenant-hub 项目的生产环境 Compose 配置: ```yaml version: "3.9" services: postgres: image: postgres:16-alpine container_name: tenant-hub-postgres restart: unless-stopped env_file: - .env.production environment: POSTGRES_DB: tenant_hub POSTGRES_USER: postgres volumes: - /home/ubuntu/tenant-hub-data/postgres:/var/lib/postgresql/data ports: - '5432:5432' healthcheck: test: ['CMD-SHELL', 'pg_isready -U postgres'] interval: 5s timeout: 3s retries: 10 api: build: context: . dockerfile: apps/api/Dockerfile target: production container_name: tenant-hub-api restart: unless-stopped env_file: - .env.production environment: NODE_ENV: production DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/tenant_hub?schema=public ports: - '4000:4000' depends_on: postgres: condition: service_healthy healthcheck: test: ['CMD-SHELL', 'wget -qO- http://localhost:4000/health || exit 1'] interval: 10s timeout: 3s retries: 12 tenant-web: build: context: . dockerfile: apps/tenant-web/Dockerfile target: production args: - VITE_API_BASE_URL=${VITE_API_BASE_URL} container_name: tenant-hub-tenant-web restart: unless-stopped ports: - '80:80' depends_on: api: condition: service_healthy healthcheck: test: ['CMD-SHELL', 'wget -qO- http://localhost:80/ || exit 1'] interval: 10s timeout: 3s retries: 12 ``` ### 服务依赖 配置中使用了 `depends_on` 的 `condition: service_healthy` 语法: - `tenant-web` 等待 `api` 健康后才启动 - `api` 等待 `postgres` 健康后才启动 这样确保服务启动顺序正确,应用不会在数据库还未就绪时尝试连接。 ### 环境变量管理 敏感配置放在 `.env.production` 文件中: ``` POSTGRES_PASSWORD=*** =*** ``` 通过 `env_file` 引入到各个服务,配置与代码分离。 --- ## 核心命令 ```bash # 启动所有服务(前台) docker compose up # 后台启动 docker compose up -d # 重新构建镜像并启动(代码变更后) docker compose up -d --build # 查看日志 docker compose logs -f docker compose logs -f api # 仅查看 api 服务 # 停止并删除容器 docker compose down # 停止并删除容器 + 卷(数据丢失) docker compose down -v # 查看状态 docker compose ps # 进入容器 docker compose exec api /bin/sh # 重启单个服务 docker compose restart api ``` --- ## 生产环境注意事项 ### 日志限制 Docker 默认日志驱动无限增长,必须限制: ```yaml services: api: logging: driver: "json-file" options: max-size: "10m" max-file: "3" ``` 或全局配置 `/etc/docker/daemon.json`: ```json { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } } ``` 修改后 `sudo systemctl restart docker` 生效。注意此操作会停止所有容器。 ### 数据库备份 ```bash # 备份 docker compose exec db pg_dump -U postgres tenant_hub > backup.sql # 恢复 docker compose exec -T db psql -U postgres tenant_hub < backup.sql ``` `-T` 禁用 TTY 分配,避免 stdin 交互问题。 定时备份脚本: ```bash #!/bin/bash DATE=$(date +%Y%m%d_%H%M%S) docker compose exec -T db pg_dump -U postgres tenant_hub > /backup/tenant_hub_${DATE}.sql find /backup -name "tenant_hub_*.sql" -mtime +7 -delete ``` ### 时区配置 容器默认 UTC 时区。同步宿主机时区: ```yaml services: api: volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro ``` ### 端口冲突 如果 80/443 被宿主机进程占用,可修改端口映射: ```yaml ports: - "8080:80" ``` 或者停止宿主机已有服务: ```bash sudo lsof -i :80 sudo ss -tlnp | grep 80 ``` ### 数据库连接问题 `depends_on` 仅保证容器启动顺序,不保证服务 ready。如果应用在数据库初始化期间尝试连接,会报错。 解决方案: 1. 在应用层实现重试逻辑 2. 添加 wait 脚本: ```bash #!/bin/sh until nc -z postgres 5432; do echo "Waiting for PostgreSQL..." sleep 1 done exec "$@" ``` 在 Dockerfile 中 CMD 改为 `CMD ["./wait-for-db.sh", "node", "dist/server.js"]` ### 权限管理 生产环境不建议使用绑定挂载。宿主机目录权限与容器内用户可能不一致,导致应用无法读写文件。优先使用 Docker 管理的命名卷。 --- ## 常用维护 ### 清理空间 ```bash docker container prune # 删除已停止容器 docker image prune -a # 删除未使用镜像 docker volume prune # 删除未使用卷 docker system prune -a --volumes # 一键清理(慎用) ``` 执行前建议 `docker system df` 查看磁盘占用。 ### 查看资源占用 ```bash docker stats ``` 实时显示所有容器的 CPU、内存、网络、IO 使用情况。 ### 镜像导出导入 ```bash docker save -o tenant-hub.tar tenant-hub-api:latest # 在目标机器加载 docker load -i tenant-hub.tar ``` 适用于内网环境无法直接 `docker pull` 的场景。 --- ## 总结 Docker 的核心价值在于环境一致性和快速部署。以 tenant-hub 项目为例,实际使用流程为: 1. 编写 Dockerfile 定义构建流程(多阶段构建、层缓存优化) 2. 编写 docker-compose.yml 定义服务编排(应用、数据库、前端、依赖关系) 3. 使用 `docker compose up -d --build` 构建并启动整个技术栈 4. 生产环境配置日志限制、健康检查、数据备份 学习路径建议先掌握单容器运行,再引入 Compose 处理多服务协作。具体问题查阅官方文档比通读大全更有效。 --- # Claude Code 实战笔记 > **类型**:笔记 > **URL**:https://nfap9.github.io/notes/claude-code-guide/ > **分类**:工程化 > **标签**:claude-code, ai-coding, 工具, ccswitch, 国产模型 > **发布日期**:2026-06-17 > **更新日期**:2026-06-18 > **描述**:Claude Code 接入国产模型、工程化配置与高效使用指南 > 把 Claude Code 当 IDE 用,而不是当聊天工具。 --- ## 接入国产模型 Anthropic Claude API 费用较高,生产环境推荐接入国产模型。编码能力与工程化效果已达到可用水平,成本却低一个数量级。 ### 模型推荐排序(2026-06) | 排名 | 模型 | 特点 | API 端点 | |------|------|------|----------| | 1 | **GLM-5.2** (智谱) | 编码能力最强,推理深度提升,适合大型工程 | `https://open.bigmodel.cn/api/paas/v4/` | | 2 | **Kimi K2.7** (月之暗面) | 长文本+推理增强,代码理解能力大幅提升 | `https://api.moonshot.cn/v1` | | 3 | **DeepSeek V4 Pro** | 数学/逻辑极强,性价比依然最高 | `https://api.deepseek.com/v1` | | 4 | **MiniMax M3** | 多模态+工具调用稳定,适合复杂 Agent 场景 | `https://api.minimax.chat/v1` | 其他备选:Qwen3-Max、Baichuan5、Step-2。建议每个模型都重试 5 分钟自己的主力项目,看看哪个在自己代码库上表现最好。 ### ccswitch 安装与配置 推荐用 `ccswitch` 管理多个模型配置,一键切换。 ```bash # 安装 npm install -g claude-settings-switch # 添加 GLM 配置 ccs add glm https://open.bigmodel.cn/api/paas/v4/ # 添加 Kimi 配置 ccs add kimi https://api.moonshot.cn/v1 # 添加 DeepSeek 配置 ccs add ds https://api.deepseek.com/v1 # 查看已有配置 ccs list # 切换到 GLM ccs sw glm # 查看当前使用的配置 ccs current ``` 配置文件存储在 `~/.ccswitcher/configs.json`,Claude Code 配置在 `~/.claude/settings.json`。 ### 手动配置(不用 ccswitch) 直接修改 `~/.claude/settings.json`: ```json { "provider": "openai", "model": "glm-4", "apiKey": "", "apiUrl": "https://open.bigmodel.cn/api/paas/v4/" } ``` 各家提供商的 OpenAI 兼容格式略有不同,具体参考对方文档。一般只需改 `apiUrl` 和 `apiKey`,`model` 填对应模型名即可。 --- ## 目录结构 Claude Code 会自动读取项目目录下的特定文件,作为项目级指令和自定义命令。 ``` 项目根目录/ ├── .claude/ │ ├── CLAUDE.md # 项目级系统指令,每次对话自动带入上下文 │ └── commands/ # 自定义命令,每个 .md 文件就是一个命令 │ ├── review.md # 代码审查 │ ├── refactor.md # 重构提示 │ └── test.md # 生成测试 ├── AGENTS.md # 项目概述、架构说明 └── ... ``` ### 文件作用 | 文件 | 作用 | 生效范围 | |------|------|---------| | `~/.claude/CLAUDE.md` | 全局系统指令 | 所有项目 | | `.claude/CLAUDE.md` | 项目级系统指令 | 当前项目 | | `.claude/commands/*.md` | 自定义命令 | 当前项目 | | `CLAUDE.md` (根目录) | 项目级指令 | 当前项目 | 项目级 `CLAUDE.md` 会覆盖全局的内容。建议每个代码库都放一个 `.claude/CLAUDE.md`。 ### AGENTS.md 链接到 CLAUDE.md 如果你已有 `AGENTS.md` 写了项目概述,不需要重复写一份。在 `CLAUDE.md` 中直接引用即可: ```markdown # 项目指令 参见项目概述: [AGENTS.md](../AGENTS.md) ## 编码规约 - 使用 TypeScript,严格模式 - 不允许使用 `any` - 所有 API 调用必须有类型定义 ``` Claude Code 会自动跟踪相对路径并读取文件。也可以用符号链接: ```bash ln -s AGENTS.md .claude/AGENTS.md ``` 然后在 `CLAUDE.md` 中引用 `./AGENTS.md`。 ### 自定义命令示例 创建 `.claude/commands/review.md`: ```markdown # 代码审查 对当前文件进行代码审查,重点关注: 1. 类型安全性 2. 错误处理 3. 性能问题 4. 可读性 输出格式:每个问题一行,带具体代码位置。 ``` 使用时输入 `/review` 即可触发。命令名就是文件名(不含 `.md`)。 --- ## 常用指令 ### 内置命令 | 指令 | 作用 | |------|------| | `/clear` | 清空当前对话上下文,不影响文件系统状态 | | `/compact` | 压缩对话历史,节省 token | | `/cost` | 查看当前会话的费用 | | `/exit` | 退出 Claude Code | | `/help` | 查看命令帮助 | | `/models` | 查看当前可用模型 | | `/terminal` | 打开终端模式(只返回命令,不解释) | ### 文件操作 | 指令 | 作用 | |------|------| | `find` | 搜索文件,支持正则 | | `read` | 读取文件内容,支持行范围 | | `edit` | 精确编辑文件,必须指定行号 | | `write` | 全重写文件 | | `grep` | 文件内容搜索 | | `ls` | 列出目录 | ### 执行命令 | 指令 | 作用 | |------|------| | `bash` | 执行 shell 命令,需要确认 | | `cat` | 查看文件内容 | | `git` | Git 操作,需要确认 | | `npm` / `pnpm` / `yarn` | 包管理操作 | Claude Code 执行可能改变状态的命令(如 `git commit`、`npm install`)时会弹出确认提示,输入 `y` 确认。 ### 实用模式 **仅读模式** 不想让 Claude 修改文件时: ``` 只读查看这个文件的结构,不要改。 ``` 或者用带引号的问句,不包含修改指令。 **任务式对话** 对于复杂任务,先让 Claude 列出计划,确认后逐步执行: ``` 实现用户登录功能,包含:1. JWT token 管理 2. 登录状态缓存 3. 登出逻辑。 请先列出实现步骤,我确认后你再开始写代码。 ``` **结果限制** 限制输出长度,避免滚屏: ``` 给出最重要的 3 个修改建议,每条不超过 50 字。 ``` --- ## 工程化配置 ### 全局 CLAUDE.md `~/.claude/CLAUDE.md` 里放通用编码规范: ```markdown # 全局编码规范 ## 通用原则 - 使用 TypeScript,严格模式 - 禁止使用 `any` - 所有函数必须有返回类型 - 所有 API 调用必须有错误处理 - 优先使用显式返回而非隐式转换 ## 文件操作 - 修改文件前先读取完整内容 - 使用精确的行号编辑,避免全重写 - 不要删除未使用的 import,除非确认无用 ``` ### 项目级 CLAUDE.md `.claude/CLAUDE.md` 里放项目特定规范: ```markdown # 项目指令 参见项目概述: [AGENTS.md](../AGENTS.md) ## 技术栈 - Next.js 15 + App Router - Prisma ORM + PostgreSQL - Tailwind CSS - pnpm monorepo ## 架构约定 - API 放在 `apps/api/src` - 组件放在 `apps/web/components` - 工具函数放在 `packages/shared` - 不跨应用直接引用内部路径 ## 提交规范 - 使用 Conventional Commits - 测试通过后才提交 ``` ### 自定义命令集 常用的自定义命令放在 `.claude/commands/`: **review.md** —— 代码审查 ```markdown # 代码审查 对当前文件进行审查,重点: 1. 类型安全 2. 错误处理 3. 性能 4. 可读性 每个问题一行,带具体位置。 ``` **refactor.md** —— 重构提示 ```markdown # 重构建议 分析当前代码,给出重构建议。 不要直接修改,只给出建议列表。 ``` **test.md** —— 生成测试 ```markdown # 生成测试 为当前文件生成单元测试,覆盖主要路径。 使用 vitest,测试文件放在同目录的 __tests__ 下。 ``` --- ## 效率技巧 ### 减少 Token 消耗 1. **使用 `/compact`** —— 对话变长后压缩历史,保留文件状态但清空对话记录 2. **小步进行** —— 每次只给一个具体任务,而非整个需求文档 3. **限制范围** —— 指定具体文件或目录,避免让 Claude 扫描整个代码库 4. **使用自定义命令** —— 封装常用模式,减少每次重复解释 ### 多模型切换策略 **开发阶段**:用国产模型做大部分工作,成本极低,效率很高。 **复杂逻辑**:遇到国产模型反复出错的复杂算法/架构问题,切换到 Claude 或 GPT-4 处理。 **审查阶段**:重要代码审查用原版 Claude,确保没有潜在问题。 ### 快捷键 | 快捷键 | 作用 | |--------|------| | Tab | 补全建议 | | Ctrl+C | 取消当前操作 | | Ctrl+D | 退出(等同 `/exit`) | | ↑ / ↓ | 浏览历史记录 | | Ctrl+R | 搜索历史记录 | --- ## 常见问题 ### Claude Code 启动失败 ```bash # 检查 Node 版本 node -v # 需要 18+ # 重新登录 claude auth login # 清理配置 rm -rf ~/.claude ``` ### 国产模型返回格式不对 部分国产模型的 OpenAI 兼容层不完整,可能出现: - 不支持 function calling → 文件操作可能出问题 - 不支持流式输出 → 响应变慢 - 不支持某些系统提示 → 行为与原版不一致 解决方案:支持较完整的模型(GLM-5.2 / Kimi K2.7 / DeepSeek V4 Pro),开发环境可以尝试更多选择。 ### 网络不稳定 国产模型 API 有时会出现超时或限流: - 配置重试逻辑(大部分 ccswitch 已内置) - 开发环境可以用本地模型(Ollama 等) - 生产环境选择有 SLA 保障的提供商 ### 文件编辑不精确 Claude Code 的编辑是基于行号的精确编辑,如果文件太大或结构复杂,可能会出现偏移。建议: - 先让 Claude 列出要修改的行号,确认后再执行 - 对于大文件,先拆分成小任务 - 修改后用 `git diff` 检查结果 --- # AI Coding:前端 UI 规范笔记 > **类型**:博客 > **URL**:https://nfap9.github.io/blog/ai-coding-ui-guidelines/ > **分类**:AI Coding > **标签**:ai-coding, ui-guidelines, frontend, skill, 设计规范 > **发布日期**:2026-06-16 > **描述**:前端在 AI Coding 中如何保证样式一致性——通过制定可被 AI 理解和遵循的 UI 规范体系,并将规范编码为 Skill 约束契约,让 Agent 写出的代码天然符合设计规范。 > 前端在 AI Coding 中如何保证样式一致性——当 AI 生成的代码散落在项目各处,用着各自不同的颜色、字号和间距时,技术债就开始以指数速度累积。本文分享一套**可被 AI 理解和遵循**的 UI 规范体系,以及如何通过 Skill 将其编码为可复用的约束契约,让 Agent 写出的代码天然符合设计规范。 --- ## 一、制定 UI 规范 ### 1.1 变量:全局通用 CSS 变量 前端样式的基础就是各种属性的值,统一色彩、字体、间距、边框、阴影这些直接与视觉相关的属性值能很好的统一视觉效果; 制定变量时应遵循以下原则: **语义化命名**:变量名描述"用途"而非"具体值"。例如 `--color-primary` 而非 `--color-blue`,`--spacing-md` 而非 `--spacing-16px`。这样当设计调整时,修改变量值即可,不需要改动所有引用。 **全面覆盖**:颜色、字体、间距、圆角、阴影、过渡等视觉维度必须全部定义,不能留空白。任何空白都会被 AI 用"魔法数字"填充。 **与框架对齐**:如果使用了第三方组件库,自定义变量应与其内部变量一一对应,仅做命名映射或值覆盖。这样 AI 在使用框架组件或自定义样式时,心智模型一致。 **有层级**:按语义组织,如每种颜色都有主色、多个浅色阶、深色阶、以及 RGB 值。让 AI 在需要 hover 背景、轻提示、重强调时都能直接选择对应的变量。 **有兼容层**:对于已有但即将废弃的历史变量,保留在兼容层中,同时引导新代码使用新变量。避免存量代码因变量删除而大面积报错。 此外,如果使用了第三方 UI 库,还需要维护一个**桥接层**,将自定义变量映射到库的内置变量。例如,使用 Element Plus 时,将 `--color-primary` 映射到 `--el-color-primary`,让框架组件在不经额外配置的情况下也自动符合品牌规范。 ### 1.2 组件:使用优先级与分层封装 组件的意义不仅能减少反复造轮子也能统一同类视图的效果,第二点在 AI Coding 尤为重要,不要"让 AI 每次都从零写",而是**用已有的封装来减少 LLM 决策**。 **组件使用优先级**: ``` 项目自定义组件 > 第三方组件库组件 > 手写组件 ``` 使用这样的策略能最大化组件复用的比例也能保证特殊视觉效果的需求 **在项目封装层解决差异,让业务层只管复用。** ### 1.3 页面布局:为典型场景制定规范 由于页面内容千变万化,我们不可能遍历所有场景,但可以最大化统一典型场景; 向 AI 定义如何划分视图**并不是一个路由页面就是一个视图,而是一个独立业务场景就是一个 layout**,实践中一个 tab 页面、一个弹窗、一个抽屉可以很容易判断为一个视图,但当页面组合了多个组件时 ai 难以区分;可根据项目中的习惯使用标题、分割线区分不同视图; --- ## 二、使用 Skill 解决 AI Coding 的风格漂移 以上规范如果停留在文档层面,只能解决人类开发者的问题。AI 不会主动去查文档,它需要被**程序化地约束**。Skill 就是这个约束机制——它是 AI 的"可加载规范",在每次生成代码时自动注入到上下文中。 ### 2.1 编写 Skill 要注意的六个要点 #### 要点一:用"禁止 + 必须"代替"建议" Skill 中的规则必须是命令式的,不能有"可以""建议""最好"这样的模糊词。AI 对模糊词的理解是弹性的,会倾向选择最省力的方案——通常就是硬编码。 给每条规则配一个**反面教材**(Forbidden)和一个**正确示范**(Required)。这让 AI 不只是知道"该做什么",还知道"什么算违规"。 例如: ``` 禁止使用硬编码颜色值。所有颜色必须使用 CSS 变量。 Forbidden: color: #0d8891; color: blue; color: rgb(0,0,0). Required: var(--color-primary); var(--text-color-regular); var(--border-color). ``` #### 要点二:提供决策树,而不是列举 当 AI 面对"我该用什么组件"时,它需要的是**决策流程**,而不是一张罗列。决策树能减少模糊地带。 例如: ``` 需要对话框/模态框? → 标准布局(标题 + 底部操作按钮)?→ 使用 BaseDialog → 自定义布局(特殊结构、无标准底部)?→ 使用 el-dialog 需要展示状态? → 二进制状态(成功/失败)?→ 使用 StatusTag → 多状态或简单标签?→ 使用 el-tag 需要展示数值统计? → 单值 + 标签 + 可选副标签?→ 使用 StatCard → 多值、图表或复杂卡片?→ 使用标准卡片类 + 自定义内容 ``` #### 要点三:Token 表按"使用场景"组织,而不是按字母顺序 AI 查 Token 时不是"我想找某个变量名",而是"我需要这个场景下的颜色/间距"。因此 Token 表必须按使用场景组织,并在表头注明用途。 例如: | 变量 | 使用场景 | |------|----------| | --color-primary | 主操作按钮、品牌标识、关键链接 | | --color-primary-light-9 | 主色的浅背景,如 hover 状态、提示框背景 | | --color-success | 成功状态、完成标记、正向指标 | | --color-success-light-9 | 成功状态标签背景 | | --spacing-sm | 表单元素间小间隙、按钮内图标与文字间距 | | --spacing-lg | 卡片内边距、模块间标准间距 | | --spacing-2xl | 页面区块间距、大卡片内边距 | #### 要点四:渐进式加载,不要一次性塞满 Skill 的上下文窗口是有限资源。不要把所有 Token 表、所有组件说明都塞进 SKILL.md 主文件。应该分层: ``` skills/ └── ui-guidelines/ ├── SKILL.md ← 第一层:触发规则 + 核心禁令(必须常驻上下文) └── references/ ├── design-tokens.md ← 第二层:完整 Token 表(需要时加载) ├── component-guide.md ← 第二层:组件决策树(需要时加载) └── layout-templates.md ← 第二层:页面布局模板(需要时加载) ``` SKILL.md 只保留最核心的禁令(约 10 条以内),确保触发时就能加载。详细的 Token 值、组件接口、布局模板放在 references 中,当 AI 需要查具体色值或组件用法时按需读取。这样可以避免每次对话都浪费大量 token 在"可能用不到"的参考信息上。 #### 要点五:全局默认值必须写入 Skill 很多规范不是通过 Token 体现,而是通过全局配置(如框架默认 props)体现的。AI 必须知道这些全局默认值,否则它会在每次使用时重复添加,或者不知道某些行为已经内置。 例如: ``` 全局默认行为(无需在组件中重复声明): - el-table 已全局开启 stripe(斑马纹),不需要在任何表格上加 stripe 属性 - el-form 已全局设置 label-position="top",不需要在任何表单上加 label-position 属性 - 表单控件标准高度为 44px,通过全局设置或变量控制 ``` 如果 AI 不知道这些全局设置,它可能在每次写表格时都加上 stripe,在每次写表单时都加上 label-position="top",导致代码冗余且破坏全局一致性。 #### 要点六:规则必须可验证,最好有代码对比 Skill 不能只写"该怎么做",还应该包含有/无 Skill 的代码对比,让 AI 能直观理解规范的实际效果。 例如,展示一段无 Skill 的违规代码(硬编码色值、字号、间距、圆角),然后列出问题清单,再展示一段有 Skill 的规范代码(全部使用变量)。这种对比不仅帮助 AI 学习,也方便人类审查时快速判断 AI 是否遵循了规范。 ### 2.2 Skill 的标准结构 综合以上要点,一个 UI 规范 Skill 的标准结构如下: **SKILL.md(主文件,常驻上下文)**包含: - 元数据(触发条件描述):让 AI 知道何时加载该 Skill - 核心禁令(约 10 条):用"禁止/必须"写成的命令式规则,每条配反面教材和正确示范 - 文件索引:告诉 AI 去哪里查更详细的 Token 表、组件决策树和布局模板 **references/design-tokens.md**:按使用场景组织的完整 Token 速查表(颜色、字体、间距、圆角、阴影、过渡、布局常量)。 **references/component-guide.md**:组件决策树 + 自定义组件列表 + 第三方组件映射 + 全局工具类索引 + Vue SFC 模板规范。 **references/layout-templates.md**:典型页面布局模板(列表页、表单页、卡片网格、仪表盘、按钮集合),每个模板包含结构示意、使用的 Token/组件、间距关系、响应式策略。 ### 2.3 迭代与维护 Skill 不是写完就冻结的文档。随着项目演进,它需要持续迭代: - **从代码审查中收集反模式**:每次发现 AI 硬编码了值,就检查是 Token 缺失还是 Skill 规则不足。如果是规则问题,更新 SKILL.md 的 Mandatory Rules。 - **组件新增时同步更新**:当项目新增自定义组件时,立即更新 component-guide.md 的决策树。否则 AI 会继续用旧的组件拼凑。 - **Token 变更时三方同步**:如果新增或修改变量,同步更新源码变量文件、桥接层、以及 design-tokens.md。三者必须一致。 - **定期用 AI 自检**:让 AI 审查现有代码,检查是否还有硬编码值。这是 Skill 的反向应用——不仅用于生成规范代码,也用于检查和修复历史代码。 --- ## 三、总结 一套好的 UI 规范 + Skill 体系应该满足: 1. **Token 全覆盖**:颜色、字体、间距、圆角、阴影、过渡全部定义,不留空白让 AI 自由发挥 2. **组件分层**:自定义组件优先于第三方库,第三方库优先于手写,每层都有清晰的迁移路径 3. **布局模板化**:典型页面有标准结构,AI 按模板组装而不是每次自由发挥 4. **Skill 命令化**:用"禁止/必须"代替"建议",用决策树代替列举,用反面教材让 AI 理解违规 5. **渐进式加载**:核心禁令常驻上下文,详细参考按需加载,不浪费 token 6. **可迭代**:从代码审查中收集反模式,从新增组件中更新决策树,从 Token 变更中同步三方文档 **规范不是给人类看的文档,而是给 AI 执行的契约。当 AI 在写代码时自动引用规范变量和自定义组件时,契约就生效了。** --- # Vue 项目代码与 UI 规范:从团队约定到可落地实践 > **类型**:博客 > **URL**:https://nfap9.github.io/blog/vue-project-code-guidelines/ > **分类**:工程化 > **标签**:vue, typescript, element-plus, 代码规范, 设计系统, 团队开发, ai-coding > **发布日期**:2026-06-16 > **描述**:总结一套面向 Vue + TypeScript + Element Plus 技术栈的项目级代码规范,覆盖组件架构、函数设计、TypeScript 约束、状态管理、API 请求层以及设计 Token 与布局模式,可直接作为团队开发约定与代码审查清单。 > 这套规范来自一个实际 Vue + TypeScript + Element Plus 项目的迭代总结。它的目标不是写出一份“完美文档”,而是让团队里的每个开发者(包括 AI Agent)在写代码时,能做出一致且可维护的决策。文中既包含原则,也包含可直接执行的行数限制、命名约定、组件选择决策树和审查清单。 --- ## 一、核心原则 在动笔写代码之前,先统一三条最基本的原则。它们会贯穿后续的每一条具体规则。 ### 1.1 单一职责 一个组件只做一件事,一个函数只做一件事。当某个文件开始同时处理数据获取、数据转换、UI 渲染和事件分发时,它就具备了被拆分的信号。 ### 1.2 禁止魔法数字 所有视觉值必须使用 CSS 变量(全局或组件级),所有逻辑中的硬编码值必须提取为具名常量。魔法数字是技术债中最隐蔽也最难批量替换的一种。 ### 1.3 优先组合而非重复 复用项目组件、工具函数和组合式函数。不要复制粘贴相似代码。重复代码会在 AI 或新人参与开发时被再次复制,导致问题呈指数扩散。 --- ## 二、代码架构规则 ### 2.1 组件大小限制 | 目标 | 最大行数 | 超出后的处理 | |------|---------|-------------| | 单个 Vue SFC | 300–400 行 | 拆分为子组件,或将逻辑移到 composable / utils | | 单个函数 | 50–60 行 | 提取为具有描述性名称的子函数 | | ` ``` - 组件特定样式用 `