AFFiNE 0.27.4 配置参数详解

AFFiNE 0.27.4 配置体系全解:环境变量与 config.json 的优先级、server/auth/mailer/storages/copilot/indexer/job/throttle 各模块字段与默认值、生效配置 appConfig 查询方法、生产模板与六类排障实录,字段清单以官方 config.schema.json 为准。

AFFiNEAFFiNE自托管2026/10/25

AFFiNE 0.27.4 配置参数详解

目标:把 AFFiNE 0.27.4 的全部配置入口讲透——config.json 有哪些模块、每个字段的默认值是什么、哪些能用环境变量覆盖、改完之后怎么验证真正生效。字段清单以官方 config.schema.json(2026-10-25 取回)为准,默认值与生效值以本文实例的 appConfig 实测为证。

一、环境信息

  • 操作系统:Ubuntu 22.04(x86_64)
  • Docker Engine:29.9.2,Docker Compose 插件 v5.6.1
  • AFFiNE 版本:0.27.4(ghcr.io/toeverything/affine:0.27.4,allinone 形态)
  • 依赖版本:PostgreSQL 16(pgvector/pgvector:pg16)、Redis 7(实际运行 7.4.11)
  • 部署目录:/opt/data/quzuo/tmp/affine/deploy/
  • 配置目录:宿主机 ./config → 容器内 /root/.affine/config
  • 数据目录:宿主机 ./data/storage → 容器内 /root/.affine/storage
  • 对外地址:http://192.168.146.128:8137
  • 管理员:admin@example.com / q1w2e3r4
  • 配置 schema:config.schema.json(62,656 字节,draft-07)
  • 部署过程见 《AFFiNE 0.27.4 Docker Compose 部署指南》

二、配置体系总览

AFFiNE 的配置分三层,优先级从高到低:

环境变量  >  config.json  >  内置默认值

三层各自的定位:

层位置适合放什么
环境变量编排文件的 environment连接串、密码、单值开关(DATABASE_URL、REDIS_SERVER_HOST、AFFINE_INDEXER_ENABLED)
config.json容器内 /root/.affine/config/config.json结构化配置(server、auth、mailer、oauth、copilot 等)
内置默认值应用代码内未显式配置时兜底,完整清单见官方 schema

三条硬规则:

  1. config.json 在进程启动时读取,修改后必须重启应用进程才生效,热加载不存在。
  2. 文件必须是合法 JSON,不能写注释、不能有尾逗号;语法错误会导致启动失败,排障时先看启动日志的解析报错。
  3. config.json 不是必填项——没有该文件时应用按默认值启动,但生产环境几乎一定要写,至少要把 server.externalUrl 配对。

同目录下还有两个伴随文件:

  • private.key:首次启动自动生成的 EC 私钥,对应 crypto.privateKey(schema 标注可用 AFFINE_PRIVATE_KEY 覆盖)。不要删,删掉会导致已签发的令牌全部失效,且多副本部署时各副本私钥不一致会互相拒绝令牌。
  • config.schema.json 不随部署生成,需要从官方 Release 下载后放在本地做编辑器校验(VSCode 的 JSON Schema 联想)。

💡 技术拓展:AFFiNE 用 JSON Schema(draft-07)描述配置,而不是自定义解析器。好处是编辑器能实时提示字段、类型与默认值;坏处是「schema 里有」不等于「当前版本已实现」——例如 auth.requireEmailVerification 默认 true,但 schema 描述明确写着 (not implemented),配了也不会真的拦截未验证邮箱。读配置时要连描述一起读,不能只看字段名。

三、配置生效值怎么查

配置是否真正生效,不用猜,登录后查 GraphQL 的 appConfig 即可拿到合并默认值后的完整生效配置。先登录拿 Cookie:

curl -s -c ai_cookies.txt -X POST http://127.0.0.1:8137/api/auth/sign-in \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"q1w2e3r4"}' \
  -o /dev/null -w "login HTTP %{http_code}\n"

实测输出:

login HTTP 200

再查生效配置:

curl -s -b ai_cookies.txt -X POST http://127.0.0.1:8137/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ appConfig }"}'

返回一个 JSON 对象,顶层键为:

redis, metrics, graphql, crypto, job, throttle, auth, mailer, doc, storages,
websocket, db, server, flags, docService, telemetry, client, calendar,
captcha, copilot, indexer, oauth, payment, worker

只看关键几项:

curl -s -b ai_cookies.txt -X POST http://127.0.0.1:8137/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"{ serverConfig { name features } }"}'

实测输出:

{
  "data": {
    "serverConfig": {
      "name": "AFFiNE 内网知识库",
      "features": ["Comment", "Copilot", "LocalWorkspace", "CopilotEmbedding"]
    }
  }
}

serverConfig.name 对应 config.json 的 server.name;features 是服务端实际开启的能力集合,Copilot 出现即证明 copilot.enabled 已被读到。这个方法比翻日志快得多,也是本文所有「生效值」的来源。

⚠️ 注意:appConfig 会返回 crypto.privateKey 明文。不要在共享终端、录屏或截图里直接展示完整响应;需要贴图时先脱敏。

四、server 模块

字段类型默认值环境变量说明
server.namestring无—服务名,桌面端连接时展示
server.externalUrlstring''—对外基础 URL,用于生成分享链接、邀请链接、头像地址
server.httpsbooleanfalseAFFINE_SERVER_HTTPS是否以 HTTPS 对外提供服务
server.hoststring'localhost'AFFINE_SERVER_HOST部署主机名(FQDN)
server.hostsarray[]—额外接受的主机名列表
server.listenAddrstring'0.0.0.0'LISTEN_ADDR监听地址
server.portnumber3010AFFINE_SERVER_PORT监听端口
server.pathstring''AFFINE_SERVER_SUB_PATH子路径部署前缀(如 /affine)

本文实例的生效值(appConfig.server 实测):

{"name":"AFFiNE 内网知识库","externalUrl":"http://192.168.146.128:8137","https":false,
 "host":"localhost","hosts":[],"listenAddr":"0.0.0.0","port":3010,"path":""}

三个容易踩的点:

  1. externalUrl 必须写对。头像 URL、邀请链接、OAuth 回调都基于它拼接。本文实例登录后返回的头像地址是 http://192.168.146.128:8137/api/avatars/...,正是 externalUrl 的值——写成 localhost 会导致其他机器打开链接失败。
  2. host 没配也显示 localhost。这是默认值,不代表实际访问地址;决定外部访问的是 externalUrl 和端口映射。不要在两者之间二选一,正确做法是 externalUrl 写实际访问地址,host 保持默认或写 FQDN。
  3. 反代场景改 https 与 path。放在 Nginx 后面且启用 TLS 时,externalUrl 用 https://域名,server.https 置 true;挂在子路径时同时设 path,并确保反代正确剥离前缀。

五、auth 模块

字段默认值说明
auth.allowSignuptrue是否允许自助注册
auth.allowSignupForOauthtrue是否允许通过 OAuth 首次登录时自动建号
auth.requireEmailDomainVerificationfalse访问受限资源前是否校验邮箱域名记录
auth.requireEmailVerificationtrue是否要求邮箱验证(schema 注明未实现)
auth.newAccountShareActionDelay86400新账号多少秒后才能邀请成员/建分享链接(24 小时)
auth.passwordRequirements.min / .max8 / 32密码长度上下限
auth.session.ttl1296000会话有效期(15 天)
auth.session.ttr604800会话刷新窗口(7 天)
auth.token.accessTokenTtl900访问令牌有效期(15 分钟)
auth.token.refreshIdleTtl2592000刷新会话闲置过期(30 天)
auth.token.refreshAbsoluteTtl15552000刷新会话绝对过期(180 天)
auth.token.refreshGracePeriod30一次性刷新轮换的并发宽限(30 秒)
auth.token.refreshRetention2592000过期刷新代的保留时长(30 天)

本文实例生效值(节选):

{"allowSignup":true,"allowSignupForOauth":true,"requireEmailDomainVerification":false,
 "requireEmailVerification":true,"newAccountShareActionDelay":86400,
 "passwordRequirements":{"min":8,"max":32},
 "session":{"ttl":1296000,"ttr":604800},
 "token":{"accessTokenTtl":900,"refreshIdleTtl":2592000,"refreshAbsoluteTtl":15552000,
          "refreshGracePeriod":30,"refreshRetention":2592000}}

生产建议:

  • 内网/团队实例把 allowSignup 关掉,成员由管理员邀请,避免陌生人注册后占用工作区配额。
  • 密码策略只改长度,不涉及复杂度校验;需要更强约束时应在外层 SSO 或反向代理做。
  • newAccountShareActionDelay 是反滥用参数,默认 24 小时;开放注册的实例不要调成 0。

登录接口本身不是 GraphQL,而是 REST:

curl -s -X POST http://127.0.0.1:8137/api/auth/sign-in \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"q1w2e3r4"}' \
  -w "\nHTTP %{http_code}\n"

实测输出:

{"id":"38ccef6f-61d0-4a00-ae0b-da659babdf06","email":"admin@example.com","avatarUrl":"http://192.168.146.128:8137/api/avatars/38ccef6f-61d0-4a00-ae0b-da659babdf06-avatar-1792902086797","name":"admin","disabled":false,"hasPassword":true,"emailVerified":false}
HTTP 200

返回体里 avatarUrl 直接暴露了 externalUrl 的拼接结果,是检查该项配置最简单的窗口。

六、mailer 模块

邮件用于邀请成员、找回密码、修改邮箱。未配置 SMTP 时这些功能会静默失败(接口成功、邮件不发出),这是自托管最常见的“功能好像坏了”。

字段默认值环境变量
mailer.SMTP.name''MAILER_SERVERNAME
mailer.SMTP.host''MAILER_HOST
mailer.SMTP.port465MAILER_PORT
mailer.SMTP.username''MAILER_USER
mailer.SMTP.password''MAILER_PASSWORD
mailer.SMTP.sender'AFFiNE Self Hosted <noreply@example.com>'MAILER_SENDER
mailer.SMTP.ignoreTLSfalseMAILER_IGNORE_TLS
mailer.fallbackDomains[]—

投递工作器参数(默认值):batchSize 50、leaseMs 120000、retentionDays 30。

fallbackSMTP 结构与 SMTP 相同,用于「按收件域名分流」:把域名加进 fallbackDomains,这些域名的邮件改走备用服务器。典型场景是主 SMTP 只允许发内部域,外部收件人走另一条通道。

配置示例:

{
  "mailer": {
    "SMTP": {
      "host": "smtp.example.com",
      "port": 465,
      "username": "noreply@example.com",
      "password": "${SMTP_PASSWORD}",
      "sender": "AFFiNE <noreply@example.com>",
      "ignoreTLS": false
    }
  }
}

注意 config.json 不支持变量插值,上面 ${SMTP_PASSWORD} 只是示意,实际应直接写值,或用环境变量 MAILER_PASSWORD 覆盖(环境变量层优先级更高,也不落盘,推荐后者)。

七、storages 模块

字段默认值说明
storages.avatar.publicPath'/api/avatars/'头像公开访问前缀
storages.avatar.storage{provider:'fs',bucket:'avatars',config:{path:'~/.affine/storage'}}头像存储
storages.blob.storage{provider:'fs',bucket:'blobs',config:{path:'~/.affine/storage'}}附件/图片/视频存储

本文实例生效值:

avatar: {publicPath:/api/avatars/, storage:{provider:fs, bucket:avatars, config:{path:~/.affine/storage}}}
blob:   {storage:{provider:fs, bucket:blobs, config:{path:~/.affine/storage}}}

~/.affine/storage 在容器内即 /root/.affine/storage,由编排文件挂到宿主机 ./data/storage。avatar 与 blob 用不同 bucket,落盘时是同级子目录,互不干扰,备份时整目录打包即可。

要换成 S3/MinIO 等对象存储,把 provider 改为对应驱动并补 config(endpoint、bucket、密钥),bucket 字段仍要保留用于逻辑隔离。切换前先迁移已有对象,否则历史头像与附件 404。

八、copilot 模块

字段默认值说明
copilot.enabledfalseAI 功能总开关
copilot.byok.enabledtrue允许工作区管理员自带模型密钥
copilot.byok.allowedProviders["openai","anthropic","gemini","fal"]允许接入的提供商
copilot.byok.allowCustomEndpointfalse是否允许自定义 API 端点
copilot.byok.allowPrivateEndpointfalse是否允许端点解析到内网地址
copilot.unsplash.key''Unsplash 图库密钥
copilot.exa.key''Exa 联网搜索密钥
copilot.storage{provider:'fs',bucket:'copilot',config:{path:'~/.affine/storage'}}AI 附件存储

本文实例把 copilot.enabled 设为 true,并开启 BYOK 与自定义端点:

{
  "copilot": {
    "enabled": true,
    "byok": { "enabled": true, "allowCustomEndpoint": true }
  }
}

对应 serverConfig.features 里出现 Copilot,说明开关已生效。

两个安全边界要留意:

  • allowCustomEndpoint 放开后,管理员可以填任意地址(包括 http:// 的内网服务);allowPrivateEndpoint 默认 false 就是拦这个。两者一起开等于允许工作区管理员做内网探测,只应在完全可信的单租户环境开启。
  • enabled: true 不等于「AI 可用」。还需要在「工作区设置 → 集成 → AI BYOK」里填至少一个提供商的密钥;没填时前端会提示未配置,而不是报服务端错误。

九、indexer 模块

全文检索是可选的,默认关闭;打开后需要一个外部搜索引擎。

字段默认值环境变量
indexer.enabledfalseAFFINE_INDEXER_ENABLED
indexer.provider.type'manticoresearch'AFFINE_INDEXER_SEARCH_PROVIDER
indexer.provider.endpoint'http://localhost:9308'AFFINE_INDEXER_SEARCH_ENDPOINT
indexer.provider.apiKey''AFFINE_INDEXER_SEARCH_API_KEY
indexer.provider.username / .password''AFFINE_INDEXER_SEA...(超长截断,见 schema)
indexer.autoIndex.batchSize10—

本文实例在编排文件里显式关闭:

environment:
  - AFFINE_INDEXER_ENABLED=false

生效值:

{"enabled":false,"provider":{"type":"manticoresearch","endpoint":"http://localhost:9308","apiKey":"","username":"","password":""},"autoIndex":{"batchSize":10}}

注意 provider 的默认值仍然存在(manticoresearch + localhost:9308),只是 enabled:false 让其不参与工作。启用前必须先部署 Manticore Search 或 Elasticsearch,否则文档索引任务会持续失败并刷日志。

十、job 与 throttle 模块

10.1 job(队列)

默认队列级参数:

字段默认值
job.queue.attempts5
job.queue.backoff.type / .delayexponential / 1000 ms
job.queue.removeOnCompletetrue
job.queue.removeOnFail.age / .count86400 / 500
job.worker{}(BullMQ WorkerOptions 透传)

各队列并发默认值:

队列并发
copilot10
calendar4
notification10
doc1
indexer1
nightly1
backendRuntime1
inviteAbuse1

队列依赖 Redis。本文实例通过 REDIS_SERVER_HOST=redis 指向同项目的 Redis 服务,appConfig.redis 实测为:

{"db":0,"host":"redis","port":6379,"username":"","password":"","ioredis":{}}

小规模部署(几十人)无需调并发;文档导入或批处理多的时候可把 doc 队列调到 2–4,但要相应增加内存——文档合并峰值约 1 GB/任务。

10.2 throttle(限流)

字段默认值说明
throttle.enabledtrue是否启用限流
throttle.throttlers.default.ttl / .limit60000 / 120普通接口:60 秒 120 次
throttle.throttlers.strict.ttl / .limit60000 / 20严格接口:60 秒 20 次

本文实例生效值:

{"enabled":true,"throttlers":{"default":{"ttl":60000,"limit":120},"strict":{"ttl":60000,"limit":20}}}

限流计数落在 Redis,多副本共享同一计数。反向代理后面部署时,要确保真实客户端 IP 能透传,否则所有请求会被算作同一个来源,更容易触发 429。

十一、websocket、doc、client、flags、telemetry

模块字段默认值说明
websockettransports["websocket","polling"]允许的传输方式
websocketmaxHttpBufferSize100000000单消息上限(约 95 MB)
docexperimental.yoctofalse用 y-octo 合并 Yjs 更新
dochistory.interval600000历史快照最小间隔(10 分钟)
clientversionControl.enabledfalse是否校验客户端版本
clientversionControl.requiredVersion'>=0.25.0'允许的版本范围
flagsallowGuestDemoWorkspacetrue游客可建演示工作区
telemetryallowedOrigin["localhost","127.0.0.1"]允许上报的来源
telemetryga4.measurementId / .apiSecret''GA4 Measurement Protocol

maxHttpBufferSize 默认值很大(100 MB),意味着单个超大附件可能占满内存。附件走的是分片上传(blob 接口),正常文档协作不会接近这个量级;如果做压力测试,建议临时调低到 10 MB 观察表现。

client.versionControl.enabled 打开后,低于 requiredVersion 的桌面/网页客户端会被拒绝,适合强制全员升级;升级期间先把范围放宽,避免把用户挡在门外。

十二、oauth、calendar、captcha、payment

这四个模块默认全关,只有填了第三方凭据才需要关注。

模块关键字段默认启用条件
oauth.providers.googleclientId / clientSecret空Google OAuth 应用
oauth.providers.githubclientId / clientSecret空GitHub OAuth App
oauth.providers.oidcclientId / clientSecret / issuer空标准 OIDC 提供商;内网 issuer 需 allowPrivateNetwork:true
oauth.providers.appleclientId / clientSecret空Apple 登录
calendar.googleenabled / clientId / clientSecret 等enabled:falseGoogle Calendar 集成
calendar.caldavenabled / allowedHosts / blockPrivateNetworkenabled:false,blockPrivateNetwork:trueCalDAV 集成
captcha.enabled布尔false防机器人;支持 Turnstile 或本地挑战(challenge.bits:20)
payment.enabled布尔false自托管不需要,保持关闭

calendar.caldav.blockPrivateNetwork 默认 true,即默认禁止连接内网 CalDAV 服务;若你的日历服务就在内网,需要显式关闭该保护并把主机加入 allowedHosts——这是一处典型的安全默认值与内网需求冲突,改动时要想清楚暴露面。

十三、配置生效验证清单

改完配置后按顺序验证,三步即可定位问题:

  1. 进程起来了没:GET /info 返回版本与形态。
curl -s -D - http://127.0.0.1:8137/info -o /tmp/ainfo.json -w "\nHTTP %{http_code}\n" | head -5
cat /tmp/ainfo.json

实测输出:

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8

{"compatibility":"0.27.4","message":"AFFiNE 0.27.4 Server","type":"selfhosted","flavor":"allinone"}
  1. 配置读到了没:登录后查 appConfig 对应模块,确认值不是默认值。本文实例 server.externalUrl 为自定义地址、copilot.enabled 为 true、indexer.enabled 为 false,三者都能在 appConfig 中一一对上。

  2. 功能特性开了没:查 serverConfig.features,Copilot 在列说明 AI 开关生效;再点进具体功能页做一次真实操作(例如邀请一位成员看邮件是否到达)。

如果第 1 步失败,看启动日志的 JSON 解析错误;如果第 2 步值没变,九成是改了宿主机的 config.json 但没重启应用进程,或者改的是另一个目录的同名文件。

十四、生产配置模板

结合本文实例与默认值,一份内网生产配置如下(SMTP 与 OAuth 按需替换,未用的模块直接省略):

{
  "$schema": "https://github.com/toeverything/affine/releases/latest/download/config.schema.json",
  "server": {
    "name": "团队知识库",
    "externalUrl": "https://affine.example.com",
    "https": true
  },
  "auth": {
    "allowSignup": false,
    "passwordRequirements": { "min": 10, "max": 32 }
  },
  "mailer": {
    "SMTP": {
      "host": "smtp.example.com",
      "port": 465,
      "username": "noreply@example.com",
      "password": "从环境变量 MAILER_PASSWORD 注入",
      "sender": "团队知识库 <noreply@example.com>"
    }
  },
  "copilot": {
    "enabled": true,
    "byok": { "enabled": true, "allowCustomEndpoint": false }
  },
  "doc": {
    "history": { "interval": 600000 }
  },
  "throttle": {
    "enabled": true
  }
}

对应编排文件里补环境变量(不落盘敏感值):

environment:
  - DATABASE_URL=postgresql://affine:<密码>@postgres:5432/affine
  - REDIS_SERVER_HOST=redis
  - MAILER_PASSWORD=<SMTP 密码>
  - AFFINE_INDEXER_ENABLED=false

十五、常见问题

15.1 改了 config.json 没反应

config.json 只在启动时读一次。确认三件事:改的是挂载进容器的那个文件(宿主机 ./config/config.json,不是镜像内副本)、JSON 语法合法、应用进程已重启。用 appConfig 对比改前改后,值不变就是没重启。

15.2 邀请链接/头像地址是 localhost

server.externalUrl 没配或配错。登录接口返回的 avatarUrl 会直接暴露这个问题:把 externalUrl 改成实际访问地址后重启,再看返回值。

15.3 邀请成员提示成功但对方没收到邮件

mailer.SMTP 为空,投递被跳过。至少配 host、port、username、password、sender 五项;自建邮件服务器用自签证书时设 ignoreTLS: true,否则 TLS 校验失败。

15.4 开启 Copilot 后 AI 仍不可用

copilot.enabled 只是总开关,具体密钥在工作区设置里配置。确认 serverConfig.features 含 Copilot,再到「工作区设置 → 集成 → AI BYOK」填提供商密钥;若使用自定义端点,还需服务端 allowCustomEndpoint: true。

15.5 打开全文搜索没有结果

indexer.enabled 仍为 false(本文实例即如此),或启用了但 Manticore/Elasticsearch 未就绪。先确认索引服务可访问 provider.endpoint,再打开开关,否则任务会持续失败。

15.6 删了 private.key 后所有人都要重新登录

private.key 是令牌签名私钥,删除后应用会重新生成,旧令牌全部失效。恢复方式是从备份还原该文件;没有备份就只能让用户重新登录。多副本部署时各副本必须共用同一把私钥,建议把整个 config 目录纳入备份。

十六、参考链接