NestJS 控制平面下的 Daytona,Agent 靠 TaoToken 补 Base URL

发布时间:2026/9/18 23:56:52
NestJS 控制平面下的 Daytona,Agent 靠 TaoToken 补 Base URL 1. 为什么我把 TaoToken 接进 Daytona 的 NestJS 控制平面我在 NestJS 控制平面里统一模型出口先到 TaoToken 官网 领取 Key再把 Base URL 固定为https://taotoken.net/api。这样 Agent 只认控制平面下发的任务模型侧不再四处填地址。过去一段时间很多团队把 Daytona 当作临时隔离执行底座Agent 生成一段脚本控制平面拉起 Sandbox执行、收集日志、销毁环境。但真正上线后会发现沙箱解决的是“代码在哪里跑”模型调用解决的是“ Agent 怎么想、怎么迭代、怎么决定下一步”。如果模型入口散落在各个 Node 进程、CI 变量、本地配置文件里后面排障会非常痛苦一次 401 要查五个地方一次模型名变更要重新发版一次额度切换要把多个服务重启。Daytona 控制平面本身用 NestJS REST API 暴露沙箱生命周期、快照、调度等能力这对后端平台工程师很友好Controller、Service、Guard、Interceptor、ConfigModule 都是熟悉的味道。我的做法不是改 Daytona 控制平面源码而是在它旁边加一层“Agent Gateway”。这层 Gateway 仍然用 NestJS 写负责三件事接收 Agent 任务校验任务 ID、语言、超时、网络策略。调 Daytona 创建隔离沙箱执行命令或脚本回收沙箱。在需要模型推理时通过统一的 TaoToken Base URL 调用模型并把结果写回任务上下文。这里的关键点是沙箱要隔离模型入口也要收口。TaoToken 在这个架构里承担的是模型 API 的统一入口Base URL 固定为https://taotoken.net/apiKey 使用占位符YOUR_API_KEY。实际落地时你可以把它放进 NestJS 的环境变量、Claude Code 的settings.json、Codex 的config.toml或者 CC Switch 的配置项里。下面我从后端平台工程师的视角把可复现的环境变量、NestJS 服务代码、沙箱调度接口和常见报错一次讲清楚。2. NestJS 环境变量TaoToken Base URL 与 Daytona Key 分离先把配置边界划清楚。很多事故不是代码写错而是把不同系统的 Key 混在一个变量里Daytona 的 API Key 用来调度沙箱TaoToken 的 Key 用来调模型两者权限、配额、审计维度完全不同。我的.env结构如下Key 全部用占位符不要提交真实值到仓库。# TaoToken 模型入口 TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_API_KEYYOUR_API_KEY # Daytona 控制平面 DAYTONA_API_URLhttps://app.daytona.io/api DAYTONA_API_KEYYOUR_DAYTONA_API_KEY DAYTONA_CREATE_PATH/sandbox DAYTONA_EXEC_PATH/sandbox/{id}/toolbox/process/execute # NestJS 基础设施 REDIS_URLredis://127.0.0.1:6379 DATABASE_URLpostgresql://user:pass127.0.0.1:5432/agent_gateway PORT3000DAYTONA_CREATE_PATH和DAYTONA_EXEC_PATH不要硬编码在 Service 里。Daytona 控制平面版本升级、部署形态变化时路径可能不同把它们放到环境变量至少不会因为一次接口调整就把整个服务重新编译。如果你在托管云上验证原型可以先到 TaoToken 官网 领取 Key再把TAOTOKEN_BASE_URL写成https://taotoken.net/api。注意Base URL 后面不要自己乱加/v1或/chat/completions具体路径交给 SDK 或你的客户端适配层处理。NestJS 里我建议用nestjs/config配合 Joi 做启动时校验。配置缺失时直接启动失败比运行到一半抛 401 更容易定位。// src/config/env.validation.ts import * as Joi from joi; export const envValidationSchema Joi.object({ NODE_ENV: Joi.string().valid(development, test, production).default(development), PORT: Joi.number().port().default(3000), TAOTOKEN_BASE_URL: Joi.string().uri().required(), TAOTOKEN_API_KEY: Joi.string().min(8).required(), DAYTONA_API_URL: Joi.string().uri().required(), DAYTONA_API_KEY: Joi.string().min(8).required(), DAYTONA_CREATE_PATH: Joi.string().required(), DAYTONA_EXEC_PATH: Joi.string().required(), REDIS_URL: Joi.string().uri().required(), DATABASE_URL: Joi.string().uri().required(), });// src/app.module.ts import { Module } from nestjs/common; import { ConfigModule } from nestjs/config; import { envValidationSchema } from ./config/env.validation; import { SandboxModule } from ./sandbox/sandbox.module; import { ModelModule } from ./model/model.module; Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: [.env.local, .env], validationSchema: envValidationSchema, validationOptions: { abortEarly: false }, }), ModelModule, SandboxModule, ], }) export class AppModule {}这里有一个实践细节不要把TAOTOKEN_API_KEY注入到沙箱内部。沙箱里跑的是模型生成的代码理论上不可信模型 Key 只应该存在于控制平面的进程环境。沙箱需要模型能力时由控制平面代理调用再把结果写回沙箱文件或返回给 Agent。这样即使沙箱内代码尝试读取环境变量也拿不到模型入口 Key。3. 控制平面模型客户端从 NestJS 到 TaoToken 的可复制配置模型调用统一封装成ModelClientService业务 Service 只依赖它不直接实例化 OpenAI、Anthropic 或其他 SDK。这样切换供应商、调整 Base URL、增加超时和重试都只改一个地方。下面用 OpenAI 兼容客户端举例Base URL 读取TAOTOKEN_BASE_URLKey 读取TAOTOKEN_API_KEY。// src/model/model-client.service.ts import { Injectable, Logger } from nestjs/common; import { ConfigService } from nestjs/config; import OpenAI from openai; export type ChatMessage { role: system | user | assistant; content: string; }; Injectable() export class ModelClientService { private readonly logger new Logger(ModelClientService.name); private readonly client: OpenAI; constructor(private readonly config: ConfigService) { this.client new OpenAI({ apiKey: this.config.getOrThrowstring(TAOTOKEN_API_KEY), baseURL: this.config.getOrThrowstring(TAOTOKEN_BASE_URL), timeout: 60_000, maxRetries: 2, }); } async chat(model: string, messages: ChatMessage[]) { this.logger.debug(model${model} baseURL${this.config.get(TAOTOKEN_BASE_URL)}); const completion await this.client.chat.completions.create({ model, messages, temperature: 0.2, }); const content completion.choices[0]?.message?.content; if (!content) { throw new Error(TaoToken returned empty completion); } return content; } }如果你的 NestJS 服务使用原生fetch而不是 OpenAI SDK建议也封装成适配器不要在每个 Controller 里手写 URL。TaoToken 的 Base URL 是https://taotoken.net/api客户端负责拼接后续路径手动拼接时最容易出现/api/v1/v1/chat/completions这类重复路径。遇到 404 时先打印最终请求 URL再检查是不是自己多拼了一层。模型名也不要写死在代码里。Agent 任务有快任务、慢任务、代码审查、摘要生成可能对应不同模型。可以在 DTO 里传model或者由控制平面根据任务类型路由。为了避免把未核实的信息写进配置示例里用model: string实际值从你的 TaoToken 控制台或 Coding Plan 页面确认。如果你在本地调试 Claude Codesettings.json可以这样配{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: YOUR_API_KEY } }注意这是 Claude Code 的配置方式变量前缀是ANTHROPIC_*。不要把这套变量名复制到 Codex。Codex 使用config.toml和它自己的 provider 配置下一节单独说。4. Claude Code、Codex、CC Switch 三件套的配置差异后端平台工程师经常同时维护几套客户端Claude Code 用来跑终端里的代码任务Codex 用来做另一种交互CC Switch 用来在多个供应商配置之间切换。三者配置格式不同最容易出错的地方就是“把 Claude 的环境变量套到 Codex 上”。下面分开写。Claude Code 侧核心是settings.json里的env{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: YOUR_API_KEY } }如果你的 Claude Code 版本还支持其他 Anthropic 兼容变量也保持同一原则Base URL 指向 TaoToken鉴权使用 TaoToken Key。不要把 Daytona 的 Key 填进去。Codex 侧使用config.toml。下面是一个 provider 配置示例env_key指向TAOTOKEN_API_KEY再由 shell 环境提供实际值model 你的模型名 model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chat对应地在终端里导出 Keyexport TAOTOKEN_API_KEYYOUR_API_KEY如果你在 Windows PowerShell 里调试可以用$env:TAOTOKEN_API_KEYYOUR_API_KEY再说 CC Switch 三件套。这里的“三件套”我理解为在 CC Switch 里新增一个供应商配置时需要填的三项核心信息供应商名称TaoToken。Base URLhttps://taotoken.net/api。API KeyYOUR_API_KEY。填完后在 Claude Code、Codex 或对应客户端之间切换时让 CC Switch 负责覆盖配置文件。这样你不需要手动改settings.json和config.toml。但要注意切换后回到客户端确认最终生效的 Base URL因为有些客户端会合并多层配置旧的环境变量可能优先级更高。5. 沙箱调度接口NestJS Controller 与 Daytona Adapter 示例现在进入控制平面核心。我的 NestJS 服务对外暴露一个内部接口Agent 不直接调 Daytona而是把任务提交给控制平面。接口设计如下POST /internal/agent/sandbox/run Content-Type: application/json { taskId: task_20250101_001, language: python, code: print(hello from sandbox), timeoutMs: 30000, allowNetwork: false, model: 你的模型名 }Controller 只做参数校验和调用 Service// src/sandbox/dto/run-sandbox.dto.ts import { IsBoolean, IsIn, IsInt, IsOptional, IsString, Max, Min, } from class-validator; export class RunSandboxDto { IsString() taskId!: string; IsIn([python, node, bash]) language!: python | node | bash; IsString() code!: string; IsInt() Min(1000) Max(120000) timeoutMs 30000; IsBoolean() allowNetwork false; IsString() IsOptional() model?: string; }// src/sandbox/sandbox.controller.ts import { Body, Controller, Post } from nestjs/common; import { RunSandboxDto } from ./dto/run-sandbox.dto; import { SandboxService } from ./sandbox.service; Controller(internal/agent/sandbox) export class SandboxController { constructor(private readonly sandboxService: SandboxService) {} Post(run) async run(Body() dto: RunSandboxDto) { return this.sandboxService.runOnce(dto); } }SandboxService负责幂等锁、创建沙箱、执行、模型总结、销毁// src/sandbox/sandbox.service.ts import { ConflictException, Injectable } from nestjs/common; import { ConfigService } from nestjs/config; import Redis from ioredis; import { DaytonaAdapter } from ./daytona.adapter; import { ModelClientService } from ../model/model-client.service; import { RunSandboxDto } from ./dto/run-sandbox.dto; Injectable() export class SandboxService { private readonly redis: Redis; constructor( private readonly config: ConfigService, private readonly daytona: DaytonaAdapter, private readonly model: ModelClientService, ) { this.redis new Redis(this.config.getOrThrowstring(REDIS_URL)); } async runOnce(dto: RunSandboxDto) { const lockKey sandbox:task:${dto.taskId}; const locked await this.redis.set(lockKey, 1, NX, PX, 120000); if (!locked) { throw new ConflictException(task ${dto.taskId} is already running); } const sandbox await this.daytona.create({ autoStopInterval: 0, allowNetwork: dto.allowNetwork, }); try { const execution await this.daytona.exec({ sandboxId: sandbox.id, language: dto.language, code: dto.code, timeoutMs: dto.timeoutMs, }); const summary await this.model.chat(dto.model ?? 你的模型名, [ { role: system, content: 你是代码执行结果分析器只根据日志判断成功、失败原因和下一步建议。, }, { role: user, content: 任务ID: ${dto.taskId}\n语言: ${dto.language}\n退出码: ${execution.exitCode}\n标准输出:\n${execution.stdout}\n标准错误:\n${execution.stderr}, }, ]); return { taskId: dto.taskId, sandboxId: sandbox.id, exitCode: execution.exitCode, stdout: execution.stdout, stderr: execution.stderr, summary, }; } finally { await this.daytona.destroy(sandbox.id); await this.redis.del(lockKey); } } }DaytonaAdapter把控制平面 HTTP 细节隔离起来。下面示例中的路径来自环境变量避免把控制平面路由写死// src/sandbox/daytona.adapter.ts import { Injectable, InternalServerErrorException } from nestjs/common; import { ConfigService } from nestjs/config; type CreateSandboxOptions { autoStopInterval: number; allowNetwork: boolean; }; type ExecOptions { sandboxId: string; language: string; code: string; timeoutMs: number; }; Injectable() export class DaytonaAdapter { constructor(private readonly config: ConfigService) {} private get baseUrl() { return this.config.getOrThrowstring(DAYTONA_API_URL); } private get headers() { return { Authorization: Bearer ${this.config.getOrThrowstring(DAYTONA_API_KEY)}, Content-Type: application/json, }; } async create(options: CreateSandboxOptions): Promise{ id: string } { const path this.config.getOrThrowstring(DAYTONA_CREATE_PATH); const response await fetch(${this.baseUrl}${path}, { method: POST, headers: this.headers, body: JSON.stringify({ auto_stop_interval: options.autoStopInterval, network: options.allowNetwork ? allow : deny, }), }); if (!response.ok) { throw new InternalServerErrorException(await response.text()); } return response.json() as Promise{ id: string }; } async exec(options: ExecOptions): Promise{ exitCode: number; stdout: string; stderr: string; } { const template this.config.getOrThrowstring(DAYTONA_EXEC_PATH); const path template.replace({id}, encodeURIComponent(options.sandboxId)); const response await fetch(${this.baseUrl}${path}, { method: POST, headers: this.headers, body: JSON.stringify({ language: options.language, code: options.code, timeout: options.timeoutMs, }), }); if (!response.ok) { throw new InternalServerErrorException(await response.text()); } return response.json() as Promise{ exitCode: number; stdout: string; stderr: string; }; } async destroy(sandboxId: string): Promisevoid { const path /sandbox/${encodeURIComponent(sandboxId)}; await fetch(${this.baseUrl}${path}, { method: DELETE, headers: this.headers, }); } }这段代码的重点不是“复制即可连接所有 Daytona 版本”而是示范控制平面应该如何分层Controller 管输入Service 管编排Adapter 管外部系统ModelClientService 管模型入口。Daytona 的具体 REST 路径请以你部署的控制平面版本为准放到环境变量里就能减少升级摩擦。6. 生命周期、自动停止与网络白名单的踩坑清单Daytona 沙箱有几个工程细节后端平台工程师必须写进控制平面而不是靠 Agent 自觉。第一默认自动停止策略。沙箱如果一段时间没有外部交互可能被自动停止以节省资源。但“不活跃”的判定通常不包含沙箱内部后台进程。如果你的任务是长时推理、批量数据处理、模型评测沙箱内部还在跑但外部没有请求就可能被中途停掉。我的做法是在创建沙箱时把auto_stop_interval设为0或者在任务执行期间定时发送心跳。上面的DaytonaAdapter.create已经把它作为参数暴露出来。第二Stop、Pause、Archive 的语义不同。Stop 更接近关机文件系统保留内存清空Pause 会把内存状态一起保存适合需要保留运行上下文的长任务Archive 把文件系统快照放到对象存储适合长期不活跃但需要保留的环境。控制平面不要只会创建和删除应该把这些状态操作封装成明确的任务动作。第三快照要版本化。把预装依赖、运行时、工具链固化到 Snapshot后续任务基于快照启动才能保证每次执行环境一致。否则 Agent 每次临时pip install、npm install既慢又不可复现。快照名称里带上语言、依赖锁文件哈希、创建时间控制平面路由时按任务需求选择。第四网络白名单默认收紧。Agent 生成的代码可能对外发起请求。控制平面默认应该禁止外网只在明确需要时开启白名单。RunSandboxDto.allowNetwork默认false需要下载依赖时走内部镜像或预构建快照而不是直接放开全部出口。第五日志要流式采集。沙箱执行可能超时、死循环、输出大量日志。控制平面至少要有最大输出限制、超时强杀、退出码记录。否则一个无限循环就能把网关内存打满。第六不要把生产数据库连接串、云厂商主账号 Key、内部管理 Token 注入沙箱。模型生成的代码在隔离环境里执行但隔离不是绝对安全。涉及 SQL、DDL、迁移、删除数据的命令请由你在本地终端手动执行不要让 Agent 在沙箱里直连生产数据库。控制平面只返回执行日志和分析结果。7. 排障401、404、超时与 Base URL 拼接错误接入 TaoToken 和 Daytona 后最常见的报错可以分成几类。401 或 403先区分是 TaoToken 还是 Daytona。模型调用返回 401检查TAOTOKEN_API_KEY是否已导出、是否有多余空格、是否误用了 Daytona Key。Claude Code 的ANTHROPIC_AUTH_TOKEN、Codex 的TAOTOKEN_API_KEY、NestJS 的TAOTOKEN_API_KEY应该是同一个 TaoToken Key但不要和DAYTONA_API_KEY混用。404模型调用 404 通常是 Base URL 或路径拼接问题。TaoToken 的 Base URL 是https://taotoken.net/api使用 OpenAI SDK 时让 SDK 自己拼路径手动fetch时先打印最终 URL。如果你看到/api/v1/v1/...就是重复拼接。Daytona 调用 404 则检查DAYTONA_API_URL和DAYTONA_CREATE_PATH是否匹配当前控制平面版本。超时模型调用超时和沙箱执行超时要分开设。ModelClientService里可以设 60 秒超时沙箱执行按任务设 30 秒到 120 秒。不要用一个全局超时覆盖两者否则模型还在生成沙箱已经被销毁。模型不存在如果返回模型未找到先去模型对话页确认当前 Key 可用的模型名再写进 DTO 或配置。不要把控制台里看到的展示名称直接当成 API 模型 ID。配置不生效Claude Code 检查settings.json的层级和 JSON 语法Codex 检查config.toml的 provider 名是否和model_provider一致CC Switch 切换后重新打开客户端。NestJS 侧可以用启动日志打印非敏感配置例如TAOTOKEN_BASE_URL和DAYTONA_API_URL但不要打印 Key。沙箱提前停止检查auto_stop_interval是否为 0任务期间是否发心跳是否被其他清理任务误删。控制平面的幂等锁也要设过期时间避免任务崩溃后锁不释放。8. CTA从模型对话到 Claude Code 文档的落地路径如果你准备把这条链路跑起来我建议按下面顺序验证不要一上来就改控制平面代码。第一步先在 模型对话 里确认 Key 和模型可用。这个页面适合做最小验证能对话说明 Base URL 和 Key 没问题。第二步根据你的使用强度看 Coding Plan。如果你只是本地调试 Claude Code、Codex可以先小规模验证如果要把 NestJS 控制平面接入生产 Agent 工作流再按并发和任务量评估。第三步到 API Keys 创建或管理 Key。把 Key 放进 NestJS 的TAOTOKEN_API_KEY不要写进代码仓库也不要注入沙箱。第四步按 Claude Code 文档 配置settings.json。如果你同时用 Codex再单独写config.toml不要把ANTHROPIC_*变量套过去。CC Switch 里则把供应商名称、Base URL、API Key 三件套填好作为多配置切换入口。最后再回到 NestJS 控制平面把TAOTOKEN_BASE_URL设为https://taotoken.net/api把TAOTOKEN_API_KEY设为YOUR_API_KEY把 Daytona 调度 Key 单独放好。启动服务后先用一条最小沙箱任务验证创建、执行、销毁再验证模型总结。整套链路跑通后你会发现 Agent 的模型入口和沙箱执行入口都被收进了同一个 NestJS 控制平面排障、审计、扩容都会清晰很多。需要统一管理入口时可以从 TaoToken 官网 开始把 Key、Base URL 和客户端配置一次理顺。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询