开始使用
配置
Mirobody 引擎如何解析配置:三层结构、敏感值自动加密,以及 config.yaml 里每个键按用途的分组说明。
引擎的每一项设置都是一个扁平的大写键。键可以来自 YAML 文件、来自远程配置服务,也可以来自进程环境变量,启动时由加载器合并成同一个命名空间。没有按模块拆分的配置文件,进程运行期间也不会热加载:改完配置需要重启。
./deploy.sh 会替你生成 .env 与 config.{env}.yaml,之后需要你亲手填的只有下面这些。其余的键都有能用的默认值,等到真的要改再回来看键清单。
| 你要填 | 在哪 | 不填会怎样 |
|---|---|---|
ENV · CONFIG_ENCRYPTION_KEY | .env | 起不来:找不到覆盖文件,或解不开加密值 |
一把 LLM key(如 OPENROUTER_API_KEY) | config.{env}.yaml | 能登录,但对话没有模型可用 |
一把 embedding key(GOOGLE_API_KEY 或 DASHSCOPE_API_KEY) | config.{env}.yaml | 可选:不配它,自由文本检索指标会退回成列出目录,显示名也保持数据源原本的拼写 |
JWT_KEY | config.{env}.yaml | 鉴权层不启用,任何人都是匿名 |
| 数据库与 Redis 连接项 | compose.yaml 已给出 | 用 Compose 时不用改;连外部实例时才要填 |
.env -> 用哪个环境,以及加密密钥config.{env}.yaml -> 你的覆盖项config.yaml -> 仓库自带的模板优先级自上而下,最先定义该键的来源胜出:
compose.yaml 的 environment:.env 里的每一个键 config.{env}.yaml 你的覆盖项(git 已忽略) JWT_KEYOPENROUTER_API_KEYMCP_PUBLIC_URL CONFIG_SERVER + CONFIG_TOKEN + ENV 三者齐备时通过 HTTP 拉取 config.yaml 仓库自带的模板(不要编辑) 一次查找会先看进程环境变量:先按原样键名找,再按大写键名找;查不到,才轮到合并后的 YAML 映射。合并本身是按加载顺序直接覆盖:config.yaml、远程配置、config.{env}.yaml,最后定义某个键的那个文件才是生效的那个。
| 文件 | 是否入库 | 由谁写 | 作用 |
|---|---|---|---|
config.yaml | 是 | 上游 | 全部键的默认值。文件开头自己就写着不要编辑本文件。 |
config.{env}.yaml | 否(.gitignore 里的 *.*.yaml 会命中它) | 你,或首次运行的 deploy.sh | 你要改的一切。ENV=localdb 时就是 config.localdb.yaml。 |
.env | 否 | 你,或 deploy.sh | ENV 决定上面那个文件;CONFIG_ENCRYPTION_KEY 用于解开加密值。 |
.env 会最先加载,其中每一行都变成环境变量,写入方式是 setdefault,所以 shell 里已经 export 的变量优先于文件。这一点值得记住:放进 .env 的任何键都压得过两层 YAML,不只是 ENV 和 CONFIG_ENCRYPTION_KEY。
# 'localdb'、'test'、'gray'、'prod',或你自己起的名字。ENV=localdb
# 最长 32 个字符。用于加密 config.{env}.yaml 里的敏感值。CONFIG_ENCRYPTION_KEY=Xk3pQ7mZ2vB9nR4tY6wL8sD1fG5hJ0aC设好三个环境变量,引擎就会在读取本地覆盖文件之前,先通过 HTTP 拉取一份已解析好的 YAML 文档:
CONFIG_SERVER=https://config.example.comCONFIG_TOKEN=<作为 X-Config-Token 请求头发送>ENV=prod实际请求是 GET {CONFIG_SERVER}/api/v1/config/environments/{ENV}/configs/resolved?is_yaml=true。三者中任一为空,或请求失败,引擎会记录原因并继续只用本地文件。远程配置在 config.{env}.yaml 之前加载,所以本地覆盖仍然胜出,适合用来在某台机器上单独钉住一个键。compose.yaml 会把 CONFIG_SERVER 和 CONFIG_TOKEN 透传进容器,所以写在 .env 里就够了。
覆盖文件里的敏感值会在第一次以明文被读到时就地加密。规则完全看键名:顶层的字符串值,只要名字匹配
_KEY _PASSWORD _PASS _PWD _SECRET _SK _TOKEN就会被加密,只有两个例外:名字以 _URL 结尾的会跳过(所以 GARMIN_TOKEN_URL 保持可读),字面量 REPLACE_THIS_VALUE_IN_PRODUCTION 也不动,模板里的占位符因此永远不会变成密文。
所以你写的是这样:
JWT_KEY: 7f2Ka9LmQ4xRt6Zv1Bn8Cs3Wd5Yh0Pj2OPENROUTER_API_KEY: sk-or-v1-abcdefGARMIN_TOKEN_URL: https://connectapi.garmin.com/oauth-service/oauth/request_token启动一次之后,磁盘上的文件变成这样:
JWT_KEY: gAAAAABm9x...已截断...Q3w==OPENROUTER_API_KEY: gAAAAABm9x...已截断...7Yk=GARMIN_TOKEN_URL: https://connectapi.garmin.com/oauth-service/oauth/request_token重写走的是 ruamel.yaml 的往返解析,所以你的注释、键顺序和引号风格都会保留。之后每次加载,加载器都会把以 gAAAA 开头的值识别为 Fernet 令牌并解密到内存里,文件本身不再变动。启动摘要里,*_API_KEY 的值会以 abc******xyz 的形式打码打印。
CONFIG_ENCRYPTION_KEY 是一个原始口令,不是 Fernet 密钥。加载器会去掉首尾空白、最多取前 32 个字符、不足 32 字节时用 0 右填充,再做 base64 编码得出 Fernet 密钥。deploy.sh 生成的是 32 位随机字母数字;任何等价做法都可以:
openssl rand -hex 16 # 32 个字符除特别说明外,下面的键都存在于 config.yaml。在那里被注释掉的键是未生效的默认值,你可以复制到自己的文件里。
| 键 | 默认值 | 说明 |
|---|---|---|
LOG_LEVEL | DEBUG | 不区分大小写。无法识别的名字回退到 INFO。 |
LOG_NAME、LOG_DIR | 未设置 | 两个都不设=只输出控制台。文件名形如 {date}_{name}_{time}.log。 |
DEFAULT_TIMEZONE | America/Los_Angeles | 用于尚未自己选时区的用户。 |
HTTP 服务
Section titled “HTTP 服务”HTTP_HOST: 0.0.0.0HTTP_PORT: 18080HTTP_ROOT: frontend
HTTP_HEADERS: Access-Control-Allow-Origin: 'http://localhost:18080' Access-Control-Allow-Credentials: 'true' Access-Control-Allow-Methods: 'GET, POST, PUT, DELETE, OPTIONS' Access-Control-Allow-Headers: 'Authorization, Content-Type' Access-Control-Max-Age: '600'
REQUEST_RATE_LIMITER: /api/chat: 6 /api/session: 6| 键 | 默认值 | 说明 |
|---|---|---|
HTTP_SERVER_NAME | mirobody | 同时作为 Server: 响应头输出,后面接版本号。 |
HTTP_HOST | 0.0.0.0 | |
HTTP_PORT | 80 | 模板里是注释掉的,所以从源码运行时若不设置就监听 80。 |
HTTP_URI_PREFIX | 空 | 把全部路由挂到某个子路径下;首尾斜杠会被规范化。 |
HTTP_ROOT | frontend | 预构建的 Web 客户端,按运行进程旁边的路径解析。它在 Python 包之外,所以 wheel 装的是引擎,而不是那堆 JavaScript;路径不存在时就是不提供客户端。 |
HTTP_HEADERS | 未设置 | 原样输出的响应头,CORS 就写在这里。注意模板自己的示例是固定 origin 搭配 Allow-Credentials,这正是浏览器要求的组合;* 与凭据同时出现会被拒绝。 |
REQUEST_RATE_LIMITER | /api/chat: 6、/api/session: 6 | 一个 { "path": 每分钟请求数 } 映射。存在且非空就会装上限流中间件,按用户在 Redis 里计数;见架构概览。 |
USER_INFO_UPDATER | 未设置 | 一组路径,命中这些路径的请求会顺带刷新调用者的档案。 |
对外 URL
Section titled “对外 URL”MCP_PUBLIC_URL 是你对外可达的基础 URL(例如一个 ngrok 域名):远程 MCP 客户端需要它,本地文件系统上的文件通过 {MCP_PUBLIC_URL}/files 提供,启动横幅也用它作为待打开的地址。见 Mirobody MCP Server。模板里另有 MCP_FRONTEND_URL、DATA_PUBLIC_URL 与 QR_LOGIN_URL 三个占位键,本版本没有生效的代码路径读它们。
必需:缓存、任务队列与限流都依赖它。
REDIS_HOST: 127.0.0.1REDIS_PORT: 18089REDIS_DB: 0REDIS_PASSWORD: ''REDIS_SSL: falseREDIS_SSL_CHECK_HOSTNAME: falseREDIS_SSL_CERT_REQS: none模板里给的是 REDIS_HOST: 10.108.0.9,即容器在 Compose 桥接网络上的地址;从宿主机访问要用 127.0.0.1 加映射出来的端口。给每个名字加后缀就声明了第二条独立连接:REDIS_HOST_LOG、REDIS_PORT_LOG 等会被请求 LOG 这条连接的代码取到。
PostgreSQL
Section titled “PostgreSQL”PG_HOST: 127.0.0.1PG_PORT: 18082PG_USER: holistic_userPG_PASSWORD: ''PG_DBNAME: holistic_dbPG_SCHEMA: theta_aiPG_ENCRYPTION_KEY: ''PG_MIN_CONNECTION: 5PG_MAX_CONNECTION: 20默认值是 PG_HOST: 10.108.0.2、用户 holistic_user、库 holistic_db、schema theta_ai、连接池 5–20。PG_ENCRYPTION_KEY 用于加密敏感列,所以它必须在第一次查询前就能解析出来。和 Redis 一样的 _后缀 写法能给你第二条连接,但有一处要注意:加密密钥是不带后缀读取的,因此带后缀的连接复用主 PG_ENCRYPTION_KEY。
S3_KEY、S3_TOKEN、S3_REGION、S3_BUCKET、S3_PREFIX、S3_CDN 在模板里都是注释掉的,而这本身就是一份可用配置:存储工厂逐个尝试云端后端,S3 后端在缺少 access key、secret、region 与 bucket 时拒绝构造,随后回退到本地文件系统。填上那四个必填键即可切到 S3 或任何兼容 S3 的存储;S3_PREFIX 限定键的命名空间,S3_CDN 是拼链接时用的公开基础 URL。加名字后缀可以选第二个 bucket。
EMAIL_PREDEFINE_CODES: caregiver@mirobody.ai: '111111'EMAIL_PREDEFINE_CODES 把邮箱映射到一个固定验证码:这些账号用该验证码登录,且不会真的发信。模板启用了三个演示账号,启动时还会汇总成一张表打印出来,所以一份全新的检出可以立刻用起来。要真正发出验证码,需要配上 EMAIL_SMTP_HOST / EMAIL_SMTP_PORT / EMAIL_SMTP_USER / EMAIL_SMTP_PASS,以及 EMAIL_FROM 与 EMAIL_FROM_NAME。
第三方登录按厂商填一块:Google 是 GOOGLE_CLIENT_ID + FIREBASE_PROJECT_ID,Apple 是 APPLE_TEAM_ID / APPLE_KEY_ID / APPLE_PRIVATE_KEY / APPLE_CLIENT_ID(App 与 Web 的 client id 不同时再加 APPLE_CLIENT_ID_APP 与 APPLE_AUTH_CLIENT_ID)。默认全部注释掉。
JWT 与 OAuth
Section titled “JWT 与 OAuth”JWT_KEY 是 HS256 的密钥,deploy.sh 首次运行时会往你的覆盖文件里写一个随机值。它同时是整套鉴权的开关:把 bearer token 解析成调用方身份的那层中间件,只有在 JWT_KEY 非空时才会被装上。JWT_PRIVATE_KEY 是 RS256 的替代方案。JWT_ISS、JWT_AUD、JWT_CLIENT_ID 与 JWT_SCOPE 会成为引擎为 MCP 客户端签发的令牌里对应的 claim。
Web 客户端配置
Section titled “Web 客户端配置”MIROBODY_WEB_CONFIG 是一个嵌套映射,运行时交给随包的 Web 客户端:__IS_*_ON__ 功能开关加上浏览器端的 Firebase 取值。整块在模板里是注释掉的。
MIROBODY_WEB_CONFIG: __IS_API_CONFIG_ON__: true __IS_QR_LOGIN_ON__: false __IS_GOOGLE_LOGIN_ON__: true __IS_APPLE_LOGIN_ON__: true __IS_NEW_FEATURES_ON__: - MCP __IS_MOBILE_SOURCE_ON__: true __FIREBASE_API_KEY__: "" __FIREBASE_AUTH_DOMAIN__: "" __FIREBASE_PROJECT_ID__: ""五个键,每个都是一组目录,告诉引擎启动时去哪里发现你自己的代码。每一项默认只有一条,也就是随包的那条。把你自己的目录加上并放在最前面:它会先于内置目录被扫描,这正是让一个部署在不改目录树的前提下覆盖随包工具或 skill 的机制。
| 键 | 默认值 | 放什么 |
|---|---|---|
MCP_TOOL_DIRS | mirobody/agent/tools | Python 模块,其顶层函数与 *Service 类会成为工具,见工具与 Agent 概览 |
MCP_RESOURCE_DIRS | mirobody/agent/resources | MCP UI 资源 |
AGENT_DIRS | mirobody/agent | agent 实现,见Agent 类型 |
PROVIDER_DIRS | mirobody/pulse/providers | 健康数据 provider,见开发一个 Provider |
SKILL_DIRS | mirobody/agent/skills | skill 目录,每个目录一个 SKILL.md,见Agent Skills |
发现动作发生在启动时,所以在任何一处新增内容都意味着加文件再重启。正斜杠会被换成当前平台的分隔符,空列表会回退到默认值。
LLM API 密钥
Section titled “LLM API 密钥”| 键 | 在哪申请 | 被谁读取 |
|---|---|---|
GOOGLE_API_KEY | aistudio.google.com/apikey | Google GenAI 客户端、文件解析、embedding |
OPENAI_API_KEY | platform.openai.com/api-keys | OpenAI 兼容客户端 |
OPENROUTER_API_KEY | openrouter.ai/keys | 一把密钥通多个模型,模板里的 provider 用的就是它 |
DASHSCOPE_API_KEY | dashscope.console.aliyun.com/apiKey | DashScope 的 OpenAI 兼容端点;也用于语音转写 |
ANTHROPIC_API_KEY | Anthropic 控制台 | 直连 Claude |
按 agent 配模型与工具
Section titled “按 agent 配模型与工具”有四组键以 agent 名字的大写形式作后缀。随包只有两个 agent,所以真正有用的后缀是 DEEP 与 BASE;模板里的 *_RTC 是一个已删除 agent 留下的空键,也不存在 *_MIX。一份最小的 PROVIDERS_DEEP 长这样:
PROVIDERS_DEEP: gemini-3.5-flash: llm_type: google-genai api_key: GOOGLE_API_KEY model: gemini-3.5-flash temperature: 1.0 claude-sonnet: llm_type: openai api_key: OPENROUTER_API_KEY base_url: https://openrouter.ai/api/v1 model: anthropic/claude-sonnet-4.6 temperature: 0.1
DISALLOWED_TOOLS_DEEP: - task| 键族 | 形态 | 说明 |
|---|---|---|
PROVIDERS_{NAME} | 显示名到 provider 配置块的映射 | 用户可选的那些名字。api_key 里放的是某个配置键名,加载时才解析:若解析不到任何值,会留下一个占位客户端来报告缺失的键名,而不是在导入阶段直接失败。 |
ALLOWED_TOOLS_{NAME} | 工具名列表 | 白名单。它优先于黑名单;两者都为空=所有被发现的工具都可用。 |
DISALLOWED_TOOLS_{NAME} | 工具名列表 | 黑名单。 |
PROMPTS_{NAME} | .jinja 路径列表 | 既可从文件系统解析,也可从已安装的包内解析。path@key 后缀用来显式指定模板名,否则用文件名当名字。 |
哪个 agent 读哪个后缀、以及各 prompt 名字对那个 agent 意味着什么,见 Agent 类型。
第三方健康平台
Section titled “第三方健康平台”GARMIN_CLIENT_ID: ''GARMIN_CLIENT_SECRET: ''GARMIN_REDIRECT_URL: ''
WHOOP_CLIENT_ID: ''WHOOP_CLIENT_SECRET: ''WHOOP_REDIRECT_URL: ''
OAUTH_TEMP_TTL_SECONDS: 900Garmin、Whoop 和 Oura 是随包 provider 里自带 OAuth 凭据的三个。注意 Oura 那三个键(OURA_CLIENT_ID、OURA_CLIENT_SECRET、OURA_REDIRECT_URL)它的 provider 会读,但模板里没有,需要你自己加。除了 client ID、secret 和回调 URL,模板还钉住了它们的端点:GARMIN_TOKEN_URL、GARMIN_AUTH_URL、GARMIN_ACCESS_TOKEN_URL、GARMIN_API_BASE_URL,以及 WHOOP_TOKEN_URL、WHOOP_AUTH_URL、WHOOP_API_BASE_URL、WHOOP_SCOPES,所以一般你只需填凭据。OAUTH_TEMP_TTL_SECONDS 限定一次待授权能保持多久有效。连接账号的过程见使用 Provider。
模板里还带着 VITAL_API_KEY 与 VITAL_ENVIRONMENT、整块 RENPHO_*,以及被注释的 FRONTIERX_CLIENT_ID / FRONTIERX_USER_POOL_ID。本版本中它们背后都没有对应的 provider 包,请当作预留键。
没有凭据的 provider 不会报错,它会拒绝启动并打印 declined to start (not configured),这是诚实状态,不是故障。唯一可以立刻试的是 PostgreSQL provider:设上 ENABLE_PGSQL_DEVICE: 1,下次启动平台就会打印 loaded 1 providers。
文件处理与指标
Section titled “文件处理与指标”| 键 | 默认值 | 说明 |
|---|---|---|
EMBEDDING_PROVIDER | gemini | gemini(需要 GOOGLE_API_KEY)或 qwen(需要 DASHSCOPE_API_KEY)。同时决定 embedding 模型和指标检索使用的向量列;未知取值会抛错。这与聊天模型用的是两把不同的 key,而且它是可选的:不配它,指标检索会退回成列出目录,显示名也不会被规整。配了但不可用时,只在 worker 日志里静默失败。见健康指标。 |
<PROVIDER>_VISION_MODEL | 各 provider 自带 | 覆盖图片与 PDF 解析所用的视觉模型,键名随 provider 变。 |
DATABASE_DECRYPTION_KEY 在模板里被标注为废弃,未来可能移除:当前的列加密密钥是 PG_ENCRYPTION_KEY。
查看已加载的配置
Section titled “查看已加载的配置”启动时引擎会打印一份已解析配置的摘要:读了哪些文件、环境名、日志与 HTTP 设置、数据存储、发现到的目录,以及找到的 API 密钥(打码)。要确认某一层是否按你预期生效,看这份摘要最快。
docker compose logs mirobody | head -40 # Compose 下curl http://localhost:18080/api/health # 工具、资源、agent 的数量如果某个值看起来是旧的,按这个顺序排查:是否有环境变量把它盖住了(包括来自 .env 的),然后你的文件名是否真的是与所设 ENV 对应的 config.{ENV}.yaml,最后键名拼写是否与加载器期望的完全一致。查找会转成大写,所以文件里的大小写无关紧要,但键名的其它部分没有任何容错。