1. eggjs 项目里 egg-mongoose 连不上 MongoDB 的真实场景如果你正在写一个 eggjs 的接口服务数据库选了 MongoDB插件用的是 egg-mongoose那么大概率会遇到这么几个瞬间npm run dev起来了但一调接口就报MongooseError: Operation buffering timed out或者connect ECONNREFUSED 127.0.0.1:27017再或者数据明明写进去了find()却返回空数组。这些问题的根子往往不在 egg-mongoose 本身而在配置的加载顺序、连接串写法、以及 model 注册时机上。这篇内容聚焦的就是这个场景一个 eggjs 项目通过 egg-mongoose 操作 MongoDB完成本地开发环境下的增删改查联调。同时我会把 TaoToken 的统一 Key 接入方式一起讲清楚——因为现在很多团队在做接口联调时模型层比如调用大模型做内容生成、字段补全和数据库层是并行开发的如果模型调用这块的 Key 管理混乱联调阶段会非常痛苦。TaoToken 在这里扮演的角色是给你一个统一的 API 通道和 Key让 eggjs 服务在调用模型能力时不用到处散落不同厂商的密钥。egg-mongoose 是什么它是 eggjs 官方生态里的 MongoDB 插件把 mongoose 的 Schema、Model 能力挂到 egg 的app.mongoose和ctx.model上让你在 controller 和 service 里直接写ctx.model.XxxModel.find()。适合谁适合已经用 eggjs 做后端、需要快速接入 MongoDB 的开发者尤其是那种「接口要跑通、数据要落库、模型调用也要接上」的联调阶段。我试过在一个 simple 模板的 egg 项目里从零配一遍踩过的坑主要集中在三处plugin.js 没开插件、config.default.js 里 mongoose 配置写成了对象而不是 client 结构、以及 model 文件名和mongoose.model()的第三个参数对不上导致查错集合。下面按可复制的顺序走一遍。2. TaoToken 统一 Key 前置准备与 eggjs 环境说明在动手写数据库配置之前先把 TaoToken 这块的前置准备好。原因很简单你的 eggjs 服务在联调阶段很可能某个 service 里要调模型接口比如给 collectdbs 里的记录自动生成摘要、或者做字段清洗这时候如果 Key 是硬编码在代码里的换环境就得改代码。TaoToken 提供的是统一 Key 统一 API 通道你只需要在配置里放一个 Key指向统一的 Base URL模型切换在控制台完成代码不用动。具体要准备的东西第一一个 TaoToken 的 API Key。去控制台的 API Keys 页面创建地址是https://taotoken.net/api-keys带 utm 的完整链接是https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite。创建后复制那串 Key形如sk-开头的一长串先存到本地环境变量里别直接写进 git。第二确认你的调用入口。TaoToken 的 API 基础地址是https://taotoken.net/api注意这个地址不加 UTM 参数直接用于代码里的baseURL。模型对话的调试页面在https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite你可以先在页面上选一个模型发一条消息确认 Key 是通的再去写代码。第三eggjs 项目环境。我用的是 egg 的 simple 脚手架Node 版本建议 16 以上。项目结构里你会用到这几个文件config/plugin.js、config/config.default.js、app/model/、app/service/、app/controller/、app/router.js。MongoDB 本地跑在127.0.0.1:27017可视化工具用 MongoDB Compass 看数据接口测试用 Postman 或 curl 都行。这里要强调一个点TaoToken 不是用来替代你的数据库的它解决的是模型调用层的 Key 统一问题。数据库还是你自己的 MongoDBegg-mongoose 还是照常连本地或你的 MongoDB 实例。两者是并行的两条线只是在同一个 eggjs 服务里共存。很多同学一开始会混淆以为接了 TaoToken 就不用配 MongoDB 了不是的。环境变量建议这样管理在项目根目录建一个.env记得加进.gitignore里面放TAOTOKEN_API_KEYsk-xxxx然后在config.default.js里通过process.env.TAOTOKEN_API_KEY读取。这样本地开发、测试、生产可以用不同的 Key代码零改动。3. 可复制的 egg-mongoose 配置与 TaoToken 接入片段这一节是核心所有片段都可以直接复制。先装依赖npm i egg-mongoose mongoose lodash dayjs注意 egg-mongoose 本身依赖 mongoose但显式装一下版本更可控。装完后按顺序改配置。3.1 config/plugin.js 开启插件/** type Egg.EggPlugin */ module.exports { mongoose: { enable: true, package: egg-mongoose, }, };这一步如果漏了启动时app.mongoose是 undefined后面所有 model 都会报错。这是最常见的第一个坑。3.2 config/config.default.js 数据库与安全配置const code require(./code.js); module.exports appInfo { const config exports {}; config.keys appInfo.name _1700000000000_0000; // egg-mongoose 连接配置注意是 client 结构 config.mongoose { client: { url: mongodb://127.0.0.1:27017/codemodel, options: { useUnifiedTopology: true, useNewUrlParser: true, }, }, }; // 关闭 csrf方便 Postman/curl 直接调 config.security { csrf: { enable: false, }, }; // 跨域配置 config.cors { origin: *, allowMethods: GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS, credentials: true, }; // 业务返回码 config.CODE code; // TaoToken 统一 Key 配置 config.taotoken { baseURL: https://taotoken.net/api, apiKey: process.env.TAOTOKEN_API_KEY || , defaultModel: claude-3-5-sonnet, }; return config; };这里config.mongoose.client.url的写法是关键。如果你写成config.mongoose { url: ... }egg-mongoose 在部分版本下不会报错但也不会连上表现就是查询一直 buffering。client这一层不能省。3.3 config/code.js 返回码module.exports { DEFAULT: { DEMO: 100001, }, };3.4 app/extend/context.js 扩展方法module.exports { success(data) { this.body { code: 0, message: success, data, }; }, error(code, message) { this.body { code, message, }; }, };3.5 app/model/collectdbs.js 定义 Modelmodule.exports app { const mongoose app.mongoose; const Schema mongoose.Schema; const CollectSchema new Schema({ filePath: { type: String }, fileName: { type: String }, fileCreateAt: { type: String }, fileUrl: { type: String }, account: { type: String }, collectName: { type: String }, fileUpdateAt: { type: String }, }); return mongoose.model(Collectdbs, CollectSchema, collectdbs); };第三个参数collectdbs是集合名必须和 MongoDB 里实际的集合名一致。如果你只写前两个参数mongoose 会把Collectdbs自动转成collectdbs复数小写大多数情况没问题但一旦你手动建了集合名不一致就会查空。3.6 app/service/collectdbs.jsconst { Service } require(egg); const _ require(lodash); class CodeModelService extends Service { async list() { const { ctx } this; return ctx.model.Collectdbs.find(); } async update(params) { const _params _(params).omitBy(_.isUndefined).omitBy(_.isNull).value(); return this.ctx.model.Collectdbs.updateOne( { _id: params._id }, _params ); } async delete(params) { return this.ctx.model.Collectdbs.deleteOne(params); } async create(params) { return this.ctx.model.Collectdbs.insertMany(params); } } module.exports CodeModelService;3.7 app/controller/collectdbs.jsconst { Controller } require(egg); const dayjs require(dayjs); class CollectdbsModel extends Controller { async list() { const { ctx, service, config } this; try { const result await service.collectdbs.list(); ctx.success(result); } catch (error) { ctx.error(config.CODE.DEFAULT.DEMO, error.message); } } async update() { const { ctx, service, config } this; const _id ctx.request.body?._id; const collectName ctx.request.body?.collectName; const updateTime dayjs().format(YYYY-MM-DD HH:mm:ss); const params { _id, collectName, fileUpdateAt: updateTime }; try { const result await service.collectdbs.update(params); ctx.success(result); } catch (error) { ctx.error(config.CODE.DEFAULT.DEMO, error.message); } } async delete() { const { ctx, service, config } this; const _id ctx.request.body?._id; try { await service.collectdbs.delete({ _id }); ctx.success({}); } catch (error) { ctx.error(config.CODE.DEFAULT.DEMO, error.message); } } async create() { const { ctx, service, config } this; const { collectName, fileName, account, fileUrl, filePath } ctx.request.body; const updateTime dayjs().format(YYYY-MM-DD HH:mm:ss); const params { collectName, fileName, account, fileUrl, filePath, fileCreateAt: updateTime, fileUpdateAt: updateTime, }; try { const result await service.collectdbs.create(params); ctx.success(result); } catch (error) { ctx.error(config.CODE.DEFAULT.DEMO, error.message); } } } module.exports CollectdbsModel;3.8 app/router.jsmodule.exports app { const { router, controller } app; router.get(/collectdbs/list, controller.collectdbs.list); router.post(/collectdbs/update, controller.collectdbs.update); router.post(/collectdbs/delete, controller.collectdbs.delete); router.post(/collectdbs/create, controller.collectdbs.create); };3.9 TaoToken 调用封装可选用于模型层联调如果你要在 service 里调模型建议单独封一个app/service/taotoken.jsconst { Service } require(egg); class TaotokenService extends Service { async chat(messages, model) { const { config } this; const res await this.ctx.curl(${config.taotoken.baseURL}/v1/chat/completions, { method: POST, contentType: json, dataType: json, headers: { Authorization: Bearer ${config.taotoken.apiKey}, }, data: { model: model || config.taotoken.defaultModel, messages, }, timeout: 60000, }); return res.data; } } module.exports TaotokenService;这样你的 eggjs 服务里数据库走 egg-mongoose模型走 TaoToken 统一通道两条线互不干扰。4. 启动项目与 curl 验证读写成功结果配置写完后启动npm run dev看到egg started on http://127.0.0.1:7001就说明起来了。如果启动时报Cannot find module egg-mongoose回去检查 plugin.js 和依赖安装。先验证写入。用 curl 发一条 createcurl -X POST http://127.0.0.1:7001/collectdbs/create \ -H Content-Type: application/json \ -d { collectName: 测试收藏, fileName: demo.md, account: tester, fileUrl: https://example.com/demo.md, filePath: /data/demo.md }预期返回{ code: 0, message: success, data: [ { _id: 65efc1b63ecbf05aacf9378b, collectName: 测试收藏, fileName: demo.md, account: tester, fileUrl: https://example.com/demo.md, filePath: /data/demo.md, fileCreateAt: 2024-03-11 10:00:00, fileUpdateAt: 2024-03-11 10:00:00, __v: 0 } ] }拿到_id后验证查询curl http://127.0.0.1:7001/collectdbs/list应该返回包含刚才那条记录的数组。如果返回空数组先去 MongoDB Compass 里看codemodel库的collectdbs集合里有没有数据。有数据但接口返回空八成是 model 的集合名对不上。验证更新curl -X POST http://127.0.0.1:7001/collectdbs/update \ -H Content-Type: application/json \ -d { _id: 65efc1b63ecbf05aacf9378b, collectName: 测试收藏-已改 }预期返回modifiedCount: 1。再去 list 一次collectName应该变了fileUpdateAt也更新成了当前时间。验证删除curl -X POST http://127.0.0.1:7001/collectdbs/delete \ -H Content-Type: application/json \ -d {_id: 65efc1b63ecbf05aacf9378b}预期返回deletedCount: 1。再 list 就查不到了。如果你同时想验证 TaoToken 通道可以在 Postman 里直接打https://taotoken.net/api/v1/chat/completionsHeader 带Authorization: Bearer sk-你的Keybody 里放model和messages返回正常就说明 Key 和通道没问题。这一步和数据库无关但联调阶段两条线都要通。5. 本篇常见错误排查401、local proxy failed、reading choices、OAuth联调阶段报错是常态这里列几个真实会遇到的对照着排。401 Unauthorized。如果你在调 TaoToken 接口时看到 401先检查 Header 里的Authorization是不是Bearer sk-xxx格式中间有没有多余空格。再检查 Key 是不是从控制台复制完整了有没有把前后引号也复制进去。还有一种情况是环境变量没生效process.env.TAOTOKEN_API_KEY是 undefined这时候请求头会变成Bearer undefined也是 401。解决方式在config.default.js里打印一下config.taotoken.apiKey的长度确认不是 0。local proxy failed。这个报错通常出现在你本地网络环境有代理设置或者 egg 的 curl 请求走了系统代理。表现是请求发不出去日志里出现local proxy failed或ECONNREFUSED。排查方向检查你的 shell 里有没有http_proxy/https_proxy环境变量有的话在启动 egg 前 unset 掉。另外 egg 的ctx.curl默认不走代理但如果你在config.default.js里配了config.httpclient相关代理要去掉。reading choices。这个报错一般出现在你解析模型返回时代码里写了res.data.choices[0]但实际返回结构不是这样或者返回体是错误信息没有choices字段。排查先把完整的res.data打印出来看结构。如果是 TaoToken 返回的错误通常会有error字段先处理错误分支再取choices。另外注意有些模型返回的是流式非流式请求才有完整choices。OAuth 相关报错。如果你在 Claude Code 或某些 CLI 工具里配置 TaoToken 时看到 OAuth 报错通常是因为工具默认走了 Anthropic 的 OAuth 流程而你需要改成 API Key 模式。以 Claude Code 为例需要设置ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY两个环境变量Base URL 指向https://taotoken.net/apiKey 用你的 TaoToken Key。如果工具里同时存在 OAuth token 和 API Key优先用 API Key把 OAuth 相关的配置清掉。MongooseError: Operation buffering timed out。这个和 TaoToken 无关是 egg-mongoose 没连上 MongoDB。检查config.mongoose.client.url里的地址端口对不对MongoDB 服务有没有起以及client这一层有没有写。如果 MongoDB 在 Docker 里注意127.0.0.1在容器内指向的是容器本身要用宿主 IP 或容器网络别名。查询返回空但 Compass 里有数据。九成是集合名不一致。mongoose.model(Collectdbs, schema, collectdbs)第三个参数必须和实际集合名完全一致大小写敏感。去 Compass 里确认集合名然后改 model 定义。csrf 报错 403。如果你没关 csrfPostman 发 POST 会返回 403。确认config.security.csrf.enable false已经配上并且重启了服务。6. 后续联调与 TaoToken 接入入口数据库这条线跑通之后你的 eggjs 服务已经能正常增删改查了。接下来如果要在业务里接模型能力比如给 collectdbs 的记录做自动分类、生成摘要、或者做字段补全就可以用前面封装的taotokenservice。Key 的管理统一在 TaoToken 控制台换模型不用改代码只改config.taotoken.defaultModel或者调用时传参。如果你还在选模型阶段想先对比不同模型对同一段 prompt 的输出效果可以直接用模型对话页面试https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite。选好模型后把模型 ID 填到配置里就行。如果你的项目进入长期编码阶段或者要跑 Agent 类的任务建议看一下 Coding Plan地址是https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite它更适合持续性的编码调用场景比按次调用更划算。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有各语言和各工具的接入示例包括 Claude Code 的配置方式。API Key 管理在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite控制台在https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite。最后说一个实操细节egg-mongoose 的 model 文件是懒加载的app/model/下的文件在第一次ctx.model.Xxx访问时才注册。所以如果你在启动阶段就想用 model得用app.model.Xxx而不是ctx.model。这个在写定时任务或者启动脚本时容易踩。另外insertMany返回的是数组updateOne返回的是{ modifiedCount, matchedCount }前端拿数据时注意结构差异别直接当对象用。