*本文由DeepSeek润色整理并上传,如果不想自己动手,建议将其发送给agent使用。
一份踩了 7 个坑才走通的实战指南。如果你照着官方文档配了 BYOK,却发现 AI 永远回一句“An error occurred, If this issue persists please let us know.”——那这篇就是为你写的。
为什么要写这篇
AFFiNE 从 0.27 开始把自建版的 AI 彻底改成 BYOK(Bring Your Own Key) 模式:服务端不再替你托管模型调用,而是由每个工作区自己填 provider 的 API Key。这个方向很合理,成本可控、数据不出自己的机器。
问题在于:官方文档给的是“理想路径”,而 0.27.x 的自建镜像里埋了七个不显眼的坑,任何一个没绕开,AI 都只会在前端界面上给你一个没有任何信息量的“发生错误”。服务端日志里才会吐出真正的线索:
Error: managed_preset_unavailable
Error: BYOK target is unavailable
Error: invalid BYOK endpoint
WARN Blocked CORS request from origin: http://your-domain:port
下面按“能跑通”的顺序,把完整的配置流程 + 每个坑的原理与解法讲清楚。DeepSeek 只是例子,任何 OpenAI 兼容的服务(LiteLLM、one-api、OpenRouter、本地 vLLM…)套路完全一样。
三个前置概念(30 秒读完)
准备工作
一台能跑 Docker 的机器(NAS / VPS / 家用服务器都行),AFFiNE stable 镜像 ≥ 0.27.4
一个 DeepSeek API Key:到 platform.deepseek.com 申请,形如 sk-xxxxxxxx(35 字符)
能 SSH 上服务器(或等价的文件编辑手段)
浏览器能正常打开你的 AFFiNE
本文假定你的 AFFiNE 由 docker-compose.yml + config/config.json 部署(官方推荐方式)。
第一步:给容器补两个环境变量(最关键)
这是 90% 的人失败的地方。AFFiNE 0.27 的服务端里其实有 两套配置读取器:Nest 那一套会读 ~/.affine/config/config.json,而处理 AI 路由的 Rust 原生运行时默认根本不看那个文件;更要命的是,它还会默认把自己当成 AFFiNE Cloud,于是对所有自建部署返回:
{ "policy": { "customEndpointMode": "unavailable" } }
结果就是:你在界面上永远找不到“自定义端点”的勾选框,硬调接口也会被 BYOK target is unavailable 顶回来。
解决办法是在 affine 服务上加两行环境变量:
name: affine
services:
affine:
image: ghcr.nio.edu.cn/toeverything/affine:stable # 用你原来的镜像地址即可
container_name: affine_server
ports:
- '3010:3010'
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_healthy
affine_migration:
condition: service_completed_successfully
volumes:
- ./data/storage:/root/.affine/storage
- ./config:/root/.affine/config
environment:
- selfhosted=true
- DEPLOYMENT_TYPE=selfhosted # ★ 告诉原生运行时:我是自建,不是云端
- AFFINE_BACKEND_RUNTIME_CONFIG_PATH=/root/.affine/config/config.json # ★ 让原生运行时也读这份配置
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://affine@postgres:5432/affine
restart: unless-stopped
背景:这几点分别对应官方 issue
#15534(原生运行时默认按 Cloud 处理)
#15506(原生运行时不读 config.json)
#15566(自建部署下 BYOK 自定义端点默认被策略关闭)
第二步:正确书写 config/config.json
坑: 官方文档给出的样例是嵌套对象:
{ "copilot": { "byok": { "enabled": true } } } // ❌ 在 0.27.4 上会被静默忽略
而这一版实际的 config schema(可以从 https://github.com/toeverything/affine/releases/latest/download/config.schema.json 拉下来看)使用的是扁平点号键。写成嵌套不会报错,只是什么都不生效——这是最阴的一个坑。
正确写法:
{
"$schema": "https://github.com/toeverything/affine/releases/latest/download/config.schema.json",
"server": {
"name": "My AFFiNE",
"externalUrl": "https://affine.example.com"
},
"copilot": {
"enabled": true,
"byok.enabled": true,
"byok.allowedProviders": ["openai", "anthropic", "gemini", "fal"],
"byok.allowCustomEndpoint": true,
"byok.allowPrivateEndpoint": false
}
}
两条要点:
byok.* 必须是扁平点号键(和 providers.openai 一个套路)。
server.externalUrl 必须等于你在浏览器里实际访问的那个域名/端口。AFFiNE 的 CORS 白名单是按它逐字构建的:externalUrl 写 https://affine.example.com,而你用 http://192.168.1.10:3010 访问,那么所有 BYOK 相关请求都会被拦掉,服务端日志里只会留下一行很容易被忽略的:
WARN [WorkspaceResolver] Blocked CORS request from origin: http://192.168.1.10:3010
(对应 issue #15552)
allowPrivateEndpoint 保持 false,除非你确实要让工作区管理员把请求指向内网地址。
改完重启服务:
docker compose up -d --force-recreate affine
# 或者(配置是挂载进去的,重启即可让进程重读)
docker restart affine_server
验证它真的生效了: 用管理员账号打开 /admin/settings/ → 左侧 AI BYOK,应当能看到「允许使用自定义 Provider 端点」为开启状态;再用管理员账号访问工作区 设置 → 集成 → AI BYOK,此时 Add key 对话框里应该出现“使用自定义 API 兼容端点”的选项——它出现,就说明前面两步都对了。
第三步:在工作区里创建 DeepSeek 档案
打开目标工作区 → Settings → Integrations → AI BYOK → Add key
填写:
Provider:OpenAI(DeepSeek 兼容 OpenAI 协议,没有专属 provider)
名称:DeepSeek(随意,好认就行)
API Key:你的 sk-xxxxxxxx
Custom endpoint / Base URL:https://api.deepseek.com/v1
模型声明填 DeepSeek 的模型 id:
点 Test key 验证(应当返回 verified),然后 Save。
若使用服务端存储(推荐,多设备可用),凭证存进 AFFiNE 的数据库;本地存储只在该设备的桌面端可用。
第四步:验证
最简单的方式:打开工作区的 Intelligence,问一句
用一句话回答:3 + 3 等于几?
能秒回「3 + 3 等于 6。」就成了 ✅
想看得更“底层”一点,可以抓 GET /api/copilot/chat/<sessionId>/stream-object,正常时会看到流式片段:
event: message
data: {"type":"text-delta","textDelta":"3"}
data: {"type":"text-delta","textDelta":" +"}
data: {"type":"text-delta","textDelta":" "}
踩坑速查表
进阶:把任何 OpenAI 兼容服务接进来
因为 DeepSeek 的接入方式本质是「openai provider + 自定义端点」,所以任何 OpenAI 兼容网关都能照抄:
模型 id 换成对方文档里的即可。
附:不用界面,直接用 GraphQL 建档案
CI / 脚本化场景可以在已登录的会话里调用(工作区设置页抓包即可看到 Cookie):
mutation($input: CreateWorkspaceByokProfileInput!) {
createWorkspaceByokProfile(input: $input) { profileId provider name enabled }
}
{
"workspaceId": "<你的工作区 id>",
"provider": "openai",
"name": "DeepSeek",
"enabled": true,
"credential": "sk-xxxxxxxx",
"definition": {
"endpoint": {
"kind": "openai_compatible",
"dialect": "chat_completions",
"url": "https://api.deepseek.com/v1"
},
"models": [
{ "modelId": "deepseek-chat", "enabled": true,
"capabilities": [{ "input": ["text"], "output": ["text", "object"],
"features": ["tool_calling"],
"attachmentKinds": [], "attachmentSources": [] }] },
{ "modelId": "deepseek-reasoner", "enabled": true,
"capabilities": [{ "input": ["text"], "output": ["text"],
"features": ["reasoning"],
"attachmentKinds": [], "attachmentSources": [] }]
}
]
}
}
验证端点与密钥(验证通过会返回 connection.kind = verified):
mutation($input: ProbeWorkspaceByokProfileInput!) {
probeWorkspaceByokProfile(input: $input) {
connection { kind errorKind testedAt }
models { modelId checks { operation status { kind errorKind } } }
}
}
小结
在 0.27+ 的 AFFiNE 自建版里接 DeepSeek,真正要记住的只有四句话:
DEPLOYMENT_TYPE=selfhosted —— 不设它,原生运行时永远当你是云端。
AFFINE_BACKEND_RUNTIME_CONFIG_PATH —— 不设它,原生运行时不读你的 config.json。
byok.* 用扁平点号键,并且 server.externalUrl 与你的访问域名逐字一致。
DeepSeek 没有专属 provider:挂 openai + openai_compatible 端点 + 声明模型 id。
这四条都对上之后,AI 会稳定工作;哪天网站搬家改了域名,别忘了回来同步 externalUrl。
祝配置顺利,少踩坑。
文中涉及的 AFFiNE 版本:0.27.4(自建 Docker 部署)。相关官方 issue:15534、15506、15552、15566。