复刻字节 AI 开发流:实践 Node.js 通用脚手架
们团队已基本停止招聘实习生今年仅招一人个人解读AI 协作模式下新人的边际效应大幅下降第二个是组织预期转变业内多位跟他同级的 Leader 私下评估今年可能出现大规模调整个人解读生成式 AI 在降本增效上的潜力被普遍看好提前做好裁员准备第三个是开发方法论的周期性层出不穷的新概念Vibe Coding、SDD 规范、Harness Engineering……理性看待这些都是过渡阶段产物随着大模型升级这些方法论的生命周期可能只有几个月个人建议不必投入过多精力去追赶因为等你熟练一种方法论后可能用不了多久就被淘汰了第四个是招聘标准演变前端岗位招聘标准很多 JD 上开始冠以全栈开发 的名义招聘了实际面试侧重前端能力考察但开始要求一些后端基础能力了个人解读AI 协作工具提效下企业希望前端承担更多职能好了八卦之后关于他们的 ai 工作流的核心如下你得教 AI不断沉淀并固化它的规范具体实践方法识别问题第一次遇到某类问题AI 执行效果不理想人工介入手动处理并解决问题规则沉淀将解决方案固化为规则或 Skill迭代积累问题解决越多效率提升越明显也就是后期 ai 执行的只要不是新的场景和复杂业务基本不会出错本质通过持续的反馈循环来训练和优化 AI 的工作能力而不是一开始就寻求完美的工作流这个理念在字节技术专家杨晨的全软软件开发大会分享中也提出过Prompt 可训练资产像模型一样优化让后我个人抽象了这个单 Agent 的工作流程图如下接下来我会解释每个步骤的思路和如何落地步骤 1项目初始化 全局规则设计项目初始化是指大多数现代框架都提供了开箱即用的脚手架工具例如# Vue/React 生态 npm create vitelatest # Nest.js nest new project-name # Hono.js pnpm create honolatest初始化完成后立即在根目录创建规则文件通常命名为 CLAUDE.md 或 AGENTS.md。这个文件是 AI 协作的宪法应包含例如项目定位技术栈清单核心哲学例如测试先行采用 TDD 测试方案项目结构示例AI 协作原则等等内容。有兴趣的同学可以找我要这个项目的全局规则。两个关键注意事项✅动态迭代规则文件不是一成不变的。当发现 AI 写的代码不规范时要想到如何抽离抽象的规则来约束而不是一味手动修改✅SKILL 机制规则的模块化当规则内容过多时抽象可复用的 Skill 文件。好处是 AI 按需加载不会每次都把全局规则加入上下文大大减少 token 用量规则中引用 SKILL 的示例## 3. 核心哲学测试先行 (TDD) 参考 .trae/skills/tdd-first/SKILL.md 中的测试驱动开发规范。 所有新功能开发或 Bug 修复必须遵循 **「红-绿-重构」** 循环。**严禁**在没有对应测试用例的情况下提交业务逻辑代码。 --- ## 4. 响应格式 参考 .trae/skills/response-standard/SKILL.md 中的响应格式规范。 **[强制]** 所有接口统一返回 JSON使用 /utils/response 中的工具函数生成响应。 ---步骤 2需求分析如果你很清楚自己要做什么可以直接下发任务。但如果只有模糊想法建议先和 AI 一起做需求分析。推荐提示词你好现在的任务是我们要从零开始设计并实现 hono.js boilerplate。 你现在是资深的 Node.js 工程师。我有一个初步的想法需要你通过向我提问帮助我澄清需求、挖掘边缘场景最终目标是理清我做一个通用后端功能的脚手架需要实现哪些功能。并按顺序输出一个实现这些功能的大纲。 请开始你的提问。效果Claude 会像资深 PM 一样向你提问你逐一回答这些问题后AI 会生成一份完整的功能清单文档。步骤 3测试先行 AI 执行代码这是整个工作流的核心环节必须让 AI 严格按照 TDD 测试方案执行代码。执行策略任务拆解→ 将需求拆分为最小可测试单元红阶段→ 先编写测试用例此时测试应当失败绿阶段→ 编写最小化实现代码使测试通过重构阶段→ 在测试通过的基础上优化代码结构和性能关键约束在全局规则中强制要求 AI✅ 任何业务代码提交前必须有对应的单元测试覆盖✅ 测试用例需包含正常场景 边界场景 异常场景✅ 测试执行必须通过覆盖率不低于 80%✅ 严禁为了赶进度而跳过测试环节步骤 4代码审核 规则反馈循环AI 执行代码后进入审核阶段分为 AI 自动审核和人工审核两部分。AI 自动审核AI 每次完成任务时需要自动进行Eslint 校验TypeScript 类型校验如果报错AI 自己修复直到通过可以限制修复次数避免死循环人工审核前期必须进行人工审核一旦发现问题思考如何抽象成规则或 Skill避免 AI 再次犯错。核心发现你会发现后期 AI 执行的效果会越来越好。对于 CRUD 场景基本不需要人审核了大概看一下产出代码就知道没问题。步骤 5持续迭代与精准化随着迭代轮次增加形成正向循环迭代轮数 ↑ ↓ 规则精确度 ↑ → AI 执行准确率 ↑ ↓ 处理边界场景的能力 ↑ ↓ 人工介入频率 ↓ ↓ 开发效率 ↑这就是 AI 时代的竞争力所在不是盲目信任 AI而是通过不断的反馈循环来「教」AI逐步固化和优化工作规范。我举个自己实践中的迭代案例例如我让 ai 实现超时中间件的时候ai 是自己原生实现的然后我感觉这种很常用的功能一般都有现成的库就查了一下果然是有 hono/timeout 这个库然后就在全局规则加入了类似”优先使用社区成熟稳定的库解决问题“。例如我在设计后端的 url 的时候突然想起 K8s 有一个类似的 url 设计规范可以跟权限结合起来例如/api/v1/roles/{roleId}其中 roles 代表就是资源roleId 代表的是子资源。resources: [roles] # 操作的资源 verbs: [get] # 操作类型增删改查 resourceNames: [{roleId}] # 可选精确到某个子资源简单就是说一个 url 代表对什么资源的什么操作。HTTP 请求RBACGET /roles/verbs: [get]GET /rolesverbs: [list]POST /rolesverbs: [create]DELETE /roles/verbs: [delete]然后就可以结合我们的 rbac 模型权限模型用来标识一个权限是什么简单来说就是资源名 操作就能标识一个权限。然后我干脆让 ai 把这个 url 规范抽象为一个 skill下次 ai 定义 url 的时候就会调用这个规则。还有很多例子就不一一列举了...需要注意的是字节内部的复杂度是更高的我们后期也会探索例如字节内部还有多 Agent 系统比如主 Agent 来负责 plan 的制定有 Coder Agent 负责编码还有测试 Agent, test Agent 等等这种多 Agent 协作我个人估计字节迟早会推出一个开源库让我们使用的所以不必着急。评测体系会对 AI 输出质量进行打分也就是评测 AI 的输出质量这条我们目前这一版是靠人工来识别但我觉得还好前期人工介入后期规则越来越好模型能力越来越强就可以进入 AI 自我评测阶段了。可观测性体系就是对于 AI 写错的地方是否能知道哪一步错了然后根据这个让 AI 自动修正 Prompt然后自动修正全局规则或者抽象为 SKILL。上面要这么玩目前个人还是很难的需要大的平台支持本文主要是先走通一个小循环也欢迎大家一起交流如果觉得不错感谢点赞关注哦欢迎入群交流~。项目实战通用后端脚手架技术栈Hono.js Drizzle ORM PostgreSQL 数据库前置声明整个工作流仅使用免费版工具Trace GLM5/豆包模型即可高质量完成充分说明该方法论的实际效果令人满意而非纸上谈兵。源码获取因外链限流问题需要的朋友可以联系我获取完整代码github脚手架目前的架构图如下第二部分企业级技术要点分享上述实战过程中网上 Node.js 教程很少提及的企业级最佳实践。优雅关闭Graceful Shutdown无论部署在 Kubernetes、Docker Compose 还是物理机都需要实现优雅关闭逻辑。为什么重要当应用报错或升级时容器编排系统会执行关闭流程应用故障/升级 → 容器启动关闭流程 ↓ 向 PID 1 进程发送 SIGTERM 信号 ↓ 开启倒计时默认 10 秒 ↓ 如果 10 秒后进程未退出发送 SIGKILL强制杀死问题场景例电商扣款业务 1. 扣除用户余额 ✅ 2. Docker 信号来了进程被强杀 3. 积分增加 ❌未执行 结果用户钱扣了但没到账投诉炸裂。问题根源Docker 强杀是瞬间的Node.js 无法执行完事件循环中的剩余回调。解决方案优雅关闭可以让你在收到信号后停止接收新请求但把内存中已排队的写操作执行完。同时及时释放系统资源如数据库连接避免占满最大连接数。链路追踪TraceIdTraceId 是请求在系统中的唯一标识符从进入系统到返回响应始终伴随整个生命周期。为什么需要场景前端用户报错 用户说我提交表单后收到错误 IDabc123def456 后端排查 ❌ 日志有 1000 条怎么找到那条错误 ✅ 按 traceId abc123def456 过滤立即定位问题Node.js 的特殊性与 Java/Go 等多线程模型相比Node.js 的单进程模型在处理 TraceId 时有本质区别语言模型上下文隔离方案难度Java/Go多线程/协程ThreadLocal⭐ 简单Node.js单进程AsyncLocalStorage⭐⭐⭐ 复杂错误做法❌方案 1全局变量let traceId; // 全局变量 app.use((req, res, next) { traceId generateId(); // 请求 A 的 traceId next(); }); // 问题请求 B 来了traceId 被覆盖日志全乱❌方案 2函数传参// controller → service → dao每层都要传 traceId // 代码极其丑陋难以维护 async function getUserOrder(traceId, userId) { const user await getUser(traceId, userId); const order await getOrder(traceId, user.id); return { user, order }; }正确方案AsyncLocalStorageNode.js 官方在async_hooks基础上封装了更高级、性能更优的 APIimport { AsyncLocalStorage } from async_hooks; const traceIdStorage new AsyncLocalStorage(); // 在请求中间件中创建隔离上下文 app.use((req, res, next) { const traceId generateId(); // 在当前上下文中存储 traceId自动隔离 traceIdStorage.run(traceId, () { next(); }); }); // 任何地方都可以取不用传参 function getTraceId() { return traceIdStorage.getStore(); } // 使用示例 async function getUserOrder(userId) { const traceId getTraceId(); // 直接取无需传参 logger.info([${traceId}] Fetching user, { userId }); const user await getUser(userId); logger.info([${traceId}] User fetched, { userId: user.id }); return user; }日志集成const logger createLogger((level, msg, meta) { const traceId getTraceId(); const logEntry { timestamp: new Date().toISOString(), level, traceId, // 自动注入 message: msg, ...meta, }; console.log(JSON.stringify(logEntry)); });TDD 测试驱动开发TDD 是企业级后端项目的核心质量保障手段在 AI 协作开发模式下更是确保代码质量的关键。核心流程红-绿-重构红阶段→ 编写测试用例预期会失败功能未实现绿阶段→ 实现最小化代码使测试通过重构阶段→ 优化代码结构保持测试通过Hono.js 项目中的实践采用 Hono 原生集成测试方案结合 Vitest 测试框架// test/user.test.ts import { describe, it, expect } from vitest; import app from ../src/app; describe(User API, () { it(should return 404 for non-existent user, async () { const res await app.request(/api/users/9999, { method: GET }); expect(res.status).toBe(404); const data await res.json(); expect(data.code).toBe(0); expect(data.message).toBe(User not found); }); it(should create a new user, async () { const res await app.request(/api/users, { method: POST, body: JSON.stringify({ name: 测试用户, email: testexample.com, password: password123 }), headers: { Content-Type: application/json } }); expect(res.status).toBe(200); const data await res.json(); expect(data.code).toBe(1); expect(data.data.name).toBe(测试用户); }); });表格驱动测试对于多分支逻辑和边界情况采用表格驱动测试风格describe(User Validation, () { const testCases [ { desc: 缺少必填字段, body: { name: 测试用户 }, expectedStatus: 400, expectedMessage: Email is required }, { desc: 邮箱格式错误, body: { name: 测试用户, email: invalid-email }, expectedStatus: 400, expectedMessage: Invalid email format }, { desc: 密码长度不足, body: { name: 测试用户, email: testexample.com, password: 123 }, expectedStatus: 400, expectedMessage: Password must be at least 6 characters } ]; test.each(testCases)($desc, async ({ body, expectedStatus, expectedMessage }) { const res await app.request(/api/users, { method: POST, body: JSON.stringify(body), headers: { Content-Type: application/json } }); expect(res.status).toBe(expectedStatus); const data await res.json(); expect(data.message).toBe(expectedMessage); }); });请求超时处理请求超时处理是后端服务稳定性的重要保障可以防止长时间运行的请求占用系统资源。为什么需要保护用户体验与其让用户等待 30 秒不如在 5 秒内返回请求超时防止系统雪崩大量超时请求堆积会导致 CPU/内存被迅速耗尽API 接口级超时利用 Hono 自带的 timeout 中间件import { timeout } from hono/timeout // 1. 全局配置所有请求默认 5 秒超时 app.use(/api/*, timeout(5000)) // 2. 局部配置针对耗时操作允许更长时间 app.get(/api/export, timeout(30000), async (c) { // 执行耗时操作... return c.json({ success: true }) }) // 3. 自定义超时后的逻辑 const customTimeout timeout(5000, { onTimeout: (c) { return c.json({ code: 0, message: 服务器繁忙请稍后再试 }, 408) } })数据库级超时API 层超时只是切断了回传给用户的路但数据库内部的任务可能仍在运行。需要更细粒度的控制// Drizzle ORM 配置通过底层驱动设置超时 import { drizzle } from drizzle-orm/postgres-js import postgres from postgres const queryClient postgres(process.env.DATABASE_URL, { timeout: 5, // 建立连接超时 (秒) idle_timeout: 20, // 空闲连接释放 max_lifetime: 60 * 30 // 连接存活最大时间 }) // 在业务代码中手动控制单次查询超时 async function getSlowData() { return await db.select().from(users).execute(); }全局错误处理在复杂的后端系统中错误可能来自业务逻辑、数据库约束、第三方 API 失败或语法错误。没有统一处理的话返回给前端的可能是难看的堆栈信息。设计原则收口原则→ 业务代码通过 throw 抛出错误由顶层中间件统一拦截处理分类分级→ 区分预期内错误和预期外错误安全性→ 生产环境下严禁将详细 Stack 返回给客户端实现方案步骤 1定义标准错误类// src/utils/errors.ts export class AppError extends Error { constructor( public statusCode: number, public message: string, public code: number 0 // 自定义业务状态码 ) { super(message); this.name AppError; } }步骤 2配置全局捕获钩子import { Hono } from hono; import { AppError } from ./utils/errors; const app new Hono(); app.onError((err, c) { const traceId c.get(traceId) || unknown; // 1. 处理已知业务异常 if (err instanceof AppError) { return c.json({ code: err.code, message: err.message, traceId }, err.statusCode as any); } // 2. 处理参数校验错误 if (err.name ZodError) { return c.json({ code: 400, message: 参数验证失败, details: err, traceId }, 400); } // 3. 处理未知错误 console.error([Fatal Error] [${traceId}]:, err); return c.json({ code: 500, message: process.env.NODE_ENV production ? 服务器内部错误 : err.message, traceId }, 500); });步骤 3业务层使用export async function deleteUser(id: string) { const user await db.findUser(id); if (!user) { throw new AppError(404, 用户不存在, 10001); } return db.delete(id); }RBAC 权限控制RBAC基于角色的访问控制是中后台系统最通用的权限模型。通过用户-角色-权限的关联实现权限的解耦。为什么不直接判断角色如果代码里写if (user.role admin)当新增一个超级编辑角色也需要此权限时得修改所有代码。判断权限点Permission而非角色名才是系统扩展性的关键。核心概念用户 (User)→ 拥有一个或多个角色角色 (Role)→ 如 Admin、Editor、Viewer权限 (Permission)→ 如 user:create、order:delete实现方案步骤 1定义数据模型// 简化版 schema export const users pgTable(users, { id: serial(id).primaryKey(), role: text(role).default(viewer), }); // 权限映射表 const ROLE_PERMISSIONS { admin: [user:all, post:all], editor: [post:edit, post:create], viewer: [post:read], } as const;步骤 2实现 RBAC 中间件// middleware/rbac.ts import { createMiddleware } from hono/factory; import { AppError } from ../utils/errors; export const checkPermission (requiredPermission: string) { return createMiddleware(async (c, next) { const user c.get(user); if (!user) { throw new AppError(401, 未授权访问); } const userPermissions ROLE_PERMISSIONS[user.role] || []; // 支持通配符或精确匹配 const hasPermission userPermissions.some(p p requiredPermission || p ${requiredPermission.split(:)[0]}:all ); if (!hasPermission) { throw new AppError(403, 权限不足无法执行此操作); } await next(); }); };步骤 3在路由层应用const api new Hono(); // 只有拥有 post:create 权限的角色才能访问 api.post(/posts, checkPermission(post:create), async (c) { return c.json({ message: 发布成功 }); }); // 管理员专属接口 api.get(/admin/stats, checkPermission(user:all), async (c) { return c.json({ stats: ... }); });日志轮转在生产环境中如果所有日志都无限制地写入同一个文件最终会导致磁盘爆满和日志文件难以打开。核心目的防止单个文件过大难以检索、占用磁盘空间自动化归档按日期分类过期清理例如只保留最近 14 天的日志实现方案Winston Daily Rotate Fileimport winston from winston; import winston-daily-rotate-file; const transport new winston.transports.DailyRotateFile({ filename: logs/application-%DATE%.log, datePattern: YYYY-MM-DD, zippedArchive: true, // 历史日志压缩 maxSize: 20m, // 单个文件超过 20MB 也会切分 maxFiles: 14d, // 只保留最近 14 天的日志 level: info, }); const logger winston.createLogger({ transports: [ transport, new winston.transports.Console() ] });应对 DDoS 攻击DDoS 攻击的本质是发大量垃圾请求导致带宽占满、CPU/内存耗尽、连接数耗尽。现实普通企业很难防住大规模 DDoS目的是提高攻击成本。限流在接入层Nginx—— 粗筛性能极高在流量进入 Node.js 之前就拦截limit_req_zone $binary_remote_addr zoneapi:10m rate10r/s; limit_req zoneapi burst20;在应用层Middleware—— 精滤灵活度高根据业务维度限流// 限制某个登录用户每分钟只能发 5 条评论 app.use(rateLimit({ windowMs: 60 * 1000, max: 5, keyGenerator: (c) c.get(user).id }));限制请求体大小防止内存溢出 (OOM)// 攻击场景发送 2GB 垃圾字符的 JSON POST 请求 // 后果Node.js 进程尝试分配 2GB 内存很快就 Out of Memory // 解决在 Nginx 层配置 client_max_body_size 1m;Helmet 安全头Helmet 通过设置各种 HTTP 响应头自动防御常见的 Web 漏洞XSS、点击劫持、MIME 类型嗅探等。性价比最高的安全加固方案。Hono.js 官方支持hono/helmet中间件在入口文件src/app.ts中引入即可import { helmet } from hono/helmet; app.use(helmet());告警机制告警机制是及时发现问题的关键通过监控关键指标在异常情况下主动通知相关人员。告警规则设计根据应用的 SLA定义不同严重等级export const alertRules [ { name: High Error Rate, condition: error_rate 5%, severity: critical, duration: 5m, action: page_oncall, // 立即电话/Slack 通知 }, { name: High Response Latency, condition: p95_latency 1000ms, severity: warning, duration: 10m, action: send_to_slack, }, { name: Database Connection Pool Exhausted, condition: db_connections 90%, severity: critical, duration: 1m, action: page_oncall, } ];与监控系统集成使用 Prometheus Alertmanager# prometheus.yml global: scrape_interval: 15s scrape_configs: - job_name: hono-app static_configs: - targets: [localhost:3000] metrics_path: /metrics alerting: alertmanagers: - static_configs: - targets: [localhost:9093]多渠道通知export async function sendAlert( title: string, message: string, severity: critical | warning | info ) { const timestamp new Date().toISOString(); // 1. Slack 通知 if (severity critical || severity warning) { await axios.post(process.env.SLACK_WEBHOOK_URL, { text: [${severity.toUpperCase()}] ${title}, attachments: [{ color: severity critical ? danger : warning, text: message, ts: Math.floor(new Date().getTime() / 1000), }], }); } // 2. 邮件通知仅限 critical if (severity critical) { await sendEmail({ to: process.env.ALERT_EMAIL, subject: CRITICAL: ${title}, html: h2${title}/h2p${message}/pp${timestamp}/p, }); } // 3. 记录到数据库 await db.insert(alerts).values({ title, message, severity, createdAt: new Date(), }); }性能测试性能测试是确保应用在生产环境中稳定运行的最后一道防线。基准测试Benchmarking使用 Autocannon 进行简单的吞吐量和延迟测试# 安装 Autocannon npm install -g autocannon # 基准测试100 并发持续 30 秒 autocannon -c 100 -d 30 http://localhost:3000/api/users # 输出示例 # Req/Sec: 1234 # Latency: { mean: 45.2, p50: 42, p95: 78, p99: 120 }压力测试Load Testing使用 K6 模拟真实用户行为// load-test.js import http from k6/http; import { check, sleep, group } from k6; export const options { stages: [ { duration: 2m, target: 100 }, { duration: 5m, target: 100 }, { duration: 2m, target: 200 }, { duration: 5m, target: 200 }, { duration: 2m, target: 0 }, ], }; export default function () { group(User API, () { // 测试获取用户列表 let listRes http.get(http://localhost:3000/api/users); check(listRes, { list status is 200: (r) r.status 200, list response time 100ms: (r) r.timings.duration 100, }); // 测试创建用户 let createRes http.post(http://localhost:3000/api/users, { name: user-${__VU}-${__ITER}, email: user-${__VU}-${__ITER}example.com, password: password123, }); check(createRes, { create status is 200: (r) r.status 200, }); sleep(1); }); }运行压力测试# 安装 K6 npm install -g k6 # 执行测试 k6 run load-test.js数据库性能测试// src/tests/db-performance.test.ts import { describe, it, expect } from vitest; import { db } from ../db; describe(Database Performance, () { it(should query 10k users in 500ms, async () { const start performance.now(); const users await db.query.users.findMany({ limit: 10000 }); const duration performance.now() - start; expect(users.length).toBe(10000); expect(duration).toBeLessThan(500); }); it(should create 1k users in batch 2s, async () { const data Array.from({ length: 1000 }, (_, i) ({ name: user-${i}, email: user-${i}example.com, password: hashed-password, })); const start performance.now(); await db.insert(users).values(data); const duration performance.now() - start; expect(duration).toBeLessThan(2000); }); });数据持久化与备份数据持久化本质上解决的是当系统崩溃、误操作、甚至被攻击时数据还能不能恢复重要认知数据库 ≠ 数据安全。数据库只是存储而备份 恢复能力才是安全的核心。备份脚本示例#!/bin/bash set -o pipefail # 核心捕获管道中任何一步的错误 DB_NAMEyour_db BACKUP_FILE/data/backups/db_$(date %Y%m%d).sql.gz # 执行备份 pg_dump -U admin -d $DB_NAME | gzip -1 $BACKUP_FILE # 检查备份是否成功 if [ $? -ne 0 ]; then echo ❌ 备份失败清理空文件... rm -f $BACKUP_FILE # 调用告警机制 # sendAlert Database Backup Failed pg_dump connection error critical exit 1 else echo ✅ 备份成功 fi可观测性Observability可观测性与监控的区别监控→ 告诉你系统出了问题基于预定义的指标和阈值可观测性→ 告诉你系统为什么出了问题通过日志、指标、链路追踪可观测性的三大支柱支柱 1结构化日志// src/utils/logger.ts import winston from winston; const logger winston.createLogger({ format: winston.format.combine( winston.format.timestamp({ format: YYYY-MM-DD HH:mm:ss }), winston.format.errors({ stack: true }), // 自定义格式化确保输出为结构化 JSON winston.format.printf(({ timestamp, level, message, ...meta }) { return JSON.stringify({ timestamp, level, traceId, message, ...meta, }); }) ), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: logs/error.log, level: error }), new winston.transports.File({ filename: logs/combined.log }), ], });支柱 2指标收集Metrics使用 Prometheus 收集性能指标// src/utils/metrics.ts import promClient from prom-client; // 创建指标 export const httpRequestDuration new promClient.Histogram({ name: http_request_duration_seconds, help: HTTP request latency, labelNames: [method, route, status_code], buckets: [0.1, 0.5, 1, 2, 5], }); export const dbQueryDuration new promClient.Histogram({ name: db_query_duration_seconds, help: Database query latency, labelNames: [operation, table], buckets: [0.01, 0.05, 0.1, 0.5, 1], }); // 暴露 Prometheus 指标端点 export function registerMetricsRoute(app: Hono) { app.get(/metrics, (c) { return c.text(promClient.register.metrics()); }); }