自托管的微信公众号图文工作台:写正文、配样式与封面、看手机预览,确认后存入你自己的公众号草稿箱。一条 docker compose up 起全套。
它能做什么 · 架构 · 快速开始 · 配置 · 微信与中转 · 备份与升级 · 已知限制 · 故障排查 · 相关项目 · 需要协助部署
- 写正文:在编辑器里写文章,粘贴的 Markdown 和图片原样保留。
- 配样式与封面:调整正文排版(字号、间距、提示块等);封面按标题自动生成,也可以上传自己的图片。
- 生成预览:产出手机尺寸的完整预览,正文与封面所见即所发。
- 存入草稿箱:确认后写入你已连接的公众号草稿箱,回到公众号后台自行发表。改稿后可以覆盖同一篇草稿,重复提交不会产生重复草稿。
只写草稿箱,不群发。 本项目不提供群发、定时群发或数据统计,也不读取已发布内容;发布与否由你在公众号后台决定。
flowchart LR B[浏览器] --> A[app 应用] A <--> P[(PostgreSQL)] A -- 任务 --> W[Worker] W -- /api/v1/wx/* --> R[微信中转 relay] R --> WX[微信公众平台 API]
docker compose up 会起四个容器:
| 服务 | 作用 | 镜像 | 对应项目 |
|---|---|---|---|
app |
编辑器、排版、预览、账号与任务管理 | ghcr.io/k7airos/wx-publish-studio:0.7.2 |
本仓库;正文编辑器用 myno,封面用 typeset-studio |
worker |
渲染封面与预览(内含 Chromium),按任务调用微信 | ghcr.io/k7airos/wx-pipeline-worker:0.24.1 |
zzhub-pipeline |
relay |
微信中转:持有账号与令牌,代理微信草稿与素材接口 | ghcr.io/k7airos/wx-relay:0.2.2 |
bun-wx-server |
db |
PostgreSQL 16 | postgres:16-alpine(固定 digest) |
官方镜像 |
三个自有镜像都发布在 GHCR,多架构(linux/amd64 与 linux/arm64),公开可拉,不需要镜像仓库账号,也不需要 docker login。
- app 镜像由本仓库的发布流程构建;worker / relay 镜像由各自仓库(zzhub-pipeline、bun-wx-server)随自己的版本发布,组件发版即时可用,不需要等本仓库发版。
- compose 默认引用本栈验证过的组合,保证开箱即稳;每个镜像同时带精确版本 tag 和
latest。 - 想始终追新:在
.env里设PIPELINE_WORKER_IMAGE=ghcr.io/k7airos/wx-pipeline-worker:latest、RELAY_IMAGE=ghcr.io/k7airos/wx-relay:latest,之后docker compose pull && docker compose up -d --wait即可滚动到组件最新版。兼容性方向是安全的:app 对 worker 做/health能力探测,新 worker 配旧 app 兼容;反方向会明确提示升级。想锁死版本:把镜像 tag 固定到精确版本号即可。
默认是模式 A(同栈自带中转):不需要自己准备中转服务。想用自己的中转见模式 B。
前置条件
- 一台 2 核 4G 起步的服务器。Worker 内含 Chromium,建议给它单独留 1G 以上内存;宿主不需要装 Chromium。
- Docker Engine 24+,
docker composev2.2 以上(--wait从 v2.2.0 才有)。 - 一个具备管理员权限、能扫码登录微信公众平台的公众号。
- Worker 能访问外网:正文里引用的外链图片由它在发布时抓取并转存到微信。
# 1. 获取代码
git clone https://github.com/k7airos/wx-publish-studio.git
cd wx-publish-studio
# 2. 准备 .env,并生成 5 个密钥(每条命令输出一个值,分别填入对应变量)
cp .env.example .env
openssl rand -hex 32 # POSTGRES_PASSWORD
openssl rand -hex 32 # CREDENTIAL_ENCRYPTION_KEY(必须是 64 位 hex)
openssl rand -hex 32 # BOOTSTRAP_SECRET(登录口令,至少 32 字符)
openssl rand -hex 32 # PIPELINE_WORKER_TOKEN
openssl rand -hex 32 # RELAY_JWT_SECRET(同栈中转的签名密钥)
# 3. 编辑 .env:填上面 5 项,并设置 APP_ORIGIN(见下方说明)
# 4. 起栈(拉取三个公开镜像,等到四个容器全部健康)
docker compose up -d --wait
# 5. 初始化同栈中转:创建中转管理员,生成访问令牌并写回 .env
bash scripts/relay-init.sh
# 6. 重新起栈,让应用加载刚写入的令牌
docker compose up -d --wait第 3 步里的 APP_ORIGIN 是你打算在浏览器里访问的 origin,必须与浏览器地址栏完全一致(协议、域名、端口):
- 本机或 SSH 隧道:
http://127.0.0.1:18889 - 自建 HTTPS 反代:
https://studio.example.com
其余变量保持 .env.example 的默认值。尤其不要改动 ZZHUB_WX_BASE_URL(留空即模式 A)和它下面两行派生变量 RELAY_MODE / RELAY_REPLICAS。
镜像拉不到时,compose 会自动退回从源码构建,不会把部署卡死。
想自己编译,或 GHCR 不可达时,把第 4 步和第 6 步的命令换成带 --build 的版本:
docker compose up -d --build --wait它会用本地代码构建应用镜像,从 k7airos/zzhub-pipeline 的固定提交构建 Worker,从 k7airos/bun-wx-server 的固定提交构建中转,并等到四个容器全部健康。首次构建要联网,冷构建约 10-20 分钟(Worker 最重,要装 Chromium 与 Noto CJK 字体)。镜像大小:应用约 1GB、Worker 约 2.1GB、中转约 250MB。有构建缓存时后续启动是秒级。
源码构建出的镜像会盖掉同名的
ghcr.io/k7airos/…标签。之后再跑不带--build的up,compose 会继续用本地那份;想换回官方镜像,先docker compose pull。
应用只绑定回环地址 127.0.0.1:18889,这是刻意的安全边界。远程访问二选一:
- SSH 隧道:
ssh -L 18889:127.0.0.1:18889 user@your-server,然后浏览器打开http://127.0.0.1:18889。 - 自建 HTTPS 反代:用 Nginx / Caddy 等终止 TLS 并转发到
127.0.0.1:18889,同时把.env的APP_ORIGIN改成你的https://域名,再重启。
换了端口要同步改 .env 的 APP_PORT 与 APP_ORIGIN。
- 登录:打开地址,输入
BOOTSTRAP_SECRET。 - 连接公众号:进「公众号」页 → 「添加公众号」,按向导填公众号名称、AppID、AppSecret(在微信公众平台的开发设置里)。模式 A 下不用填中转令牌。
- 配置 IP 白名单:向导会要你把实际调用微信的那个中转的出口公网 IP 加进公众号后台的「API IP 白名单」。模式 A 下中转就跑在这台服务器上,所以填这台服务器的公网 IP(例如
203.0.113.10)。白名单生效有等待期,第一次存入草稿箱时才会真正验证。 - 写第一篇:进「文章」页 → 新建 → 在编辑器里写正文,可以直接粘贴现成的 Markdown。
- 配排版与封面:右侧面板调样式;封面可按标题自动生成,也可以上传自己的图。
- 预览:点「预览」,看手机尺寸下的完整效果。
- 存入草稿箱:确认无误后点「存入草稿箱」,回到公众号后台的草稿箱就能看到。
上面某一步出错,先看故障排查。更完整的部署细节见 自托管说明。
不想用随栈的中转,就用你自己的(需要实现 /api/v1/wx/* 契约,见微信与中转里的 7 个端点):
- 在
.env里就地修改ZZHUB_WX_BASE_URL那一行,填你的中转地址(例如https://relay.example.com)。不要把它追加到文件末尾,下面两行派生变量依赖它的取值。 RELAY_MODE与RELAY_REPLICAS保持原样:它们是派生变量,模式 B 下自动把中转副本数置 0。- 执行
docker compose up -d --wait(用已发布镜像)或docker compose up -d --build --wait(从源码构建)。compose 不会起中转容器,Worker 指向你的地址。 PUBLISHING_RELAY_PAT留空,使用者在连接向导里填自己中转的 PAT;RELAY_JWT_SECRET与scripts/relay-init.sh都用不到。IP 白名单填你自己中转的出口 IP。
模式 B 下 compose 仍会准备中转镜像(只是不运行),这样不必维护第二套构建分支;不想准备就删掉或注释掉 compose 里的 relay 服务。
所有配置都在启动时读取一次,所以改任何环境变量都必须重启(重建)容器才生效:用已发布镜像是 docker compose up -d --wait,从源码构建是 docker compose up -d --build --wait。
docker compose 对下面 5 项用 :? 直接报错并指出缺哪一项,不会静默使用默认值:
| 变量 | 含义 | 怎么得到 |
|---|---|---|
APP_ORIGIN |
受信浏览器 origin,必须与浏览器实际 origin 完全一致 | 手填,例如 http://127.0.0.1:18889 或 https://studio.example.com |
POSTGRES_PASSWORD |
数据库密码(用十六进制,避免 URL 编码歧义) | openssl rand -hex 32 |
CREDENTIAL_ENCRYPTION_KEY |
公众号凭据加密主密钥,64 位 hex。更换后旧凭据无法解密,必须先备份并从备份恢复,或重新连接全部公众号 | openssl rand -hex 32 |
BOOTSTRAP_SECRET |
登录口令,bootstrap 模式至少 32 字符 | openssl rand -hex 32 |
PIPELINE_WORKER_TOKEN |
app 与 Worker 共用的令牌 | openssl rand -hex 32 |
另有一项按模式必填:RELAY_JWT_SECRET,仅模式 A 需要(同栈中转的签名密钥,openssl rand -hex 32);模式 B 下中转不启动,不需要它。
compose 只检查「有没有填」。填了但不合规的值由应用启动时校验并报明确原因,例如 BOOTSTRAP_SECRET must contain at least 32 characters、CREDENTIAL_ENCRYPTION_KEY must be 32-byte hexadecimal、APP_ORIGIN must be an HTTPS origin, or an explicit HTTP loopback origin。
可选配置一览
| 变量 | 含义 |
|---|---|
ZZHUB_WX_BASE_URL |
留空 = 同栈自带中转(模式 A);填写 = 外部中转地址(模式 B) |
PUBLISHING_RELAY_PAT |
部署统一持有的中转访问令牌。模式 A 由 scripts/relay-init.sh 写入 |
PUBLISHING_WX_WHITELIST_IP |
微信 API 白名单需要的出口公网 IP;配置后界面直接显示该值 |
PUBLISHING_AUTH_MODE |
bootstrap(默认)或 oauth,见接入统一身份 |
PUBLISHING_OAUTH_* |
oauth 模式下的身份源配置:issuer、client id/secret、redirect,以及标准发现或三个显式端点 |
PUBLISHING_COPY_MODE |
客户可见文案形态:neutral(默认)/ hosted,见部署形态与客户可见文案 |
PUBLISHING_BODY_IMAGE_SOURCE_ORIGIN / PUBLISHING_BODY_IMAGE_SOURCE_PREFIX |
正文里从外部地址导入图片时允许的来源域名与路径前缀,例如 https://img.example.com + /owner/hash/。留空 = 不允许导入外部图片,正文图片只能靠编辑器上传 |
PUBLISHING_BODY_IMAGE_OWNER_SOURCES |
按使用者(owner)单独授权的图片来源,JSON 形式:{"<owner-uuid>":[{"origin":"https://img.example.com","pathPrefix":"/their/files/"}]}。用于迁移既有素材 |
APP_PORT |
宿主端口,默认 18889;改了要同步改 APP_ORIGIN |
APP_IMAGE |
覆盖应用镜像引用,默认 ghcr.io/k7airos/wx-publish-studio:0.7.2。固定到别的版本或自己的 registry 时填它,必须写完整 registry 路径 |
PIPELINE_WORKER_IMAGE |
覆盖 Worker 镜像引用,默认 ghcr.io/k7airos/wx-pipeline-worker:0.24.1 |
RELAY_IMAGE |
覆盖中转镜像引用,默认 ghcr.io/k7airos/wx-relay:0.2.2 |
RELAY_ADMIN_USERNAME / RELAY_ADMIN_PASSWORD |
同栈中转的管理员账号,供 scripts/relay-init.sh 使用,脚本自己读写 .env;没有口令时会随机生成,请自行妥善保管 |
RELAY_MODE 与 RELAY_REPLICAS 是由 ZZHUB_WX_BASE_URL 推导的派生变量,请保留它们,并保持它们位于 ZZHUB_WX_BASE_URL 之后。
如果配置了 PUBLISHING_RELAY_PAT,使用者在界面上只需填 AppID 与 AppSecret;不配置时,需要在连接向导里填写自己中转服务的访问令牌(PAT)。如果之后把 PUBLISHING_RELAY_PAT 从环境变量里撤掉,早先建立的连接会缺少 PAT,连接列表会提示「这个连接没有中转访问令牌(PAT)」,需要逐个在「更新密钥」里补齐。
- 两种模式:模式 A(默认)由 compose 起第 4 个容器
relay,地址是http://relay:3001,只在容器网络内可达,不映射宿主端口;模式 B 指向你自己的中转地址,compose 不起中转容器。 - 中转从哪来:模式 A 用已发布的
ghcr.io/k7airos/wx-relay(源码为 bun-wx-server,也可让 compose 从它的固定提交现场构建);模式 B 可以自己实现/api/v1/wx/*契约。 - 覆盖范围:随栈中转已实现 Pipeline 需要的全部 7 个微信端点:
cgi-bin/token、material/add_material、draft/add、draft/update、draft/get、draft/delete、draft/batchget。并支持Idempotency-Key幂等去重:同一把 PAT 下同 key 的重复请求不会重复写微信,直接返回首次结果;默认保留 24 小时,可用WX_IDEMPOTENCY_TTL_SECONDS调整。模式 B 请自行确认你的中转实现了同样的契约。 - PAT:PAT 是中转服务签发的访问令牌,代表你调用中转接口,与微信 AppID / AppSecret 是两回事。scope 里
wx对应/api/v1/wx/。模式 A 下bash scripts/relay-init.sh会创建管理员、生成带wxscope 的 PAT 并写回.env的PUBLISHING_RELAY_PAT;重复跑是安全的:已有令牌仍有效就沿用,需要换新令牌时旧的同名令牌会被吊销,不会越积越多。 - IP 白名单:在公众号后台的「API IP 白名单」里填实际调用微信的那个中转的出口公网 IP:模式 A 是这台服务器的公网 IP,模式 B 才是你自己中转的出口 IP。白名单生效有等待期,填错不会在保存连接时报错,而是在第一次存入草稿箱时暴露。
- 正文图片:不需要自备对象存储。在编辑器里上传的图片存在应用自己的数据卷,发布时随任务交给 Worker,由 Worker 上传到微信,草稿里的图片地址是微信自己的 CDN。正文里直接引用的外链图片,Worker 会在发布时从公网抓取并转存到微信,所以 Worker 要能访问外网。只有主题里的固定素材指向远程地址时,才需要另外准备一个公网可达的 HTTPS 地址。
四个卷各存不同的东西:
| 卷 | 内容 |
|---|---|
app-data |
应用私有数据:上传的封面与正文素材(assets/、body-assets/) |
database-data |
PostgreSQL 数据:文章、排版、封面模板、公众号连接、加密凭据修订、任务映射 |
worker-data |
Worker 的账号登记、任务队列与运行结果 |
relay-data |
同栈中转的数据:管理员、访问令牌、审计日志(模式 B 下没有这个卷) |
备份要包含这四个卷,以及 CREDENTIAL_ENCRYPTION_KEY。少任何一项,恢复后都无法完整还原。
不要执行
docker compose down -v。-v会删除上述四个卷,用户数据不可恢复。
升级(数据库迁移由应用在启动时自动执行,记录在 schema_migrations 表里)。用已发布镜像:
git pull
docker compose pull
docker compose up -d --wait用源码构建:
git pull
docker compose up -d --build --wait-
换版本时要重新拉镜像:compose 里的默认标签是写死的版本号,标签没变时
up不会重新拉。升级后先docker compose pull,再up。 -
源码构建路径下,Worker 与中转镜像来自两个公开仓库的固定提交,compose 会复用构建缓存;缓存失效时耗时按上面「方式二」估计。
-
如果把
APP_IMAGE指向了自己的 registry(如registry.example.com/wx-publish-studio:1.2.3),就改成先推再拉:docker build -t registry.example.com/wx-publish-studio:1.2.3 . docker push registry.example.com/wx-publish-studio:1.2.3 docker compose pull && docker compose up -d --wait
- 单实例:app 与 Worker 都不能跑多副本。Worker 是本地 JSON 队列,共享卷多副本会互相覆盖。
- 同栈中转只在容器网络内可达:模式 A 的中转不映射宿主端口,只能被同栈 Worker 通过
http://relay:3001访问。 - 默认是 bootstrap 单用户:只有一个访问口令。通用 OIDC 可以接入,但多用户配额、邀请制与自助开通成员仍未提供,OIDC 登录的用户需要管理员预先绑定成员。
- 无注册、无群发:不提供账号体系,也不提供任何群发能力。
- 只写草稿箱:不读取公众号已发布内容,不做数据统计。
- 默认文案已中性化:上游编辑器包
@akairos/myno的默认主题与插件示例里带有某个具体公众号的名称,这些名称曾随分发产物发布,现在由构建期通用中性化清成中性文案(例如页脚默认文案为「你的公众号」),运行镜像内/app/dist已 0 命中。规则在scripts/brand-copy.ts,升级上游包后必须复核;残留任何上游默认名称时构建直接失败。自查:bun run build && bun run check:brand。
展开故障排查表
| 现象 | 排查方向 |
|---|---|
git clone 404 |
确认克隆地址是 https://github.com/k7airos/wx-publish-studio.git。 |
docker compose 报 required variable ... is missing |
.env 里漏填或填成了空行,compose 会指出缺哪一个。模式 A 下常漏的是 RELAY_JWT_SECRET。 |
manifest unknown / pull access denied |
镜像标签写错了(多半是 .env 里手抄错了),或镜像还没发布。先直接 docker pull <标签>,报错信息最准;也可以在 GHCR 页面看已发布的标签。不存在就改回 compose 默认值,或改走源码构建。 |
unknown flag: --wait |
Compose 版本太旧(--wait 从 v2.2.0 才有)。升级后重试;实在升不了就去掉 --wait,改用 docker compose up -d 再用 docker compose ps 自己看健康状态。 |
端口已被占用(Bind for 127.0.0.1:18889 failed) |
宿主上这个端口已有别的进程或另一套栈。改 .env 的 APP_PORT(例如 18890),并把 APP_ORIGIN 同步改成同一个端口。 |
| 登录总是失败 / 提示来源不合法 | APP_ORIGIN 与浏览器地址栏的 origin 必须完全一致(协议、域名、端口)。最常见是协议写成 https 而实际是 http,或 SSH 隧道换端口后没同步 APP_PORT 与 APP_ORIGIN。 |
首启后改了 POSTGRES_PASSWORD,应用连不上库 |
数据库卷里已经用旧密码建好的角色还在。改密码要用数据库自身的口令修改方式,或清掉 database-data 卷重来(会丢数据)。 |
构建时卡在拉公开仓库 / could not read from remote repository |
Worker 与中转镜像从 GitHub 公开仓库以 git context 构建,检查网络、代理与 DNS。 |
一直卡在 waiting for ... to be healthy |
看 docker compose logs worker、docker compose logs relay 的具体报错。 |
| Worker 起不来 / 一直不健康 | docker compose logs worker:先看是不是连不上 ZZHUB_WX_BASE_URL,再看中转是否已启动。 |
| Worker 镜像拉不到 | 默认是公开镜像 ghcr.io/k7airos/wx-pipeline-worker:0.24.1,拉不动多半是网络问题。也可以用 PIPELINE_WORKER_IMAGE 指向自己构建的镜像,或改走源码构建。 |
| 中转起不来 / 一直不健康 | docker compose logs relay:模式 A 下先确认 .env 里有 RELAY_JWT_SECRET。 |
scripts/relay-init.sh 报 中转容器没有在运行 |
先执行 docker compose up -d --wait。 |
scripts/relay-init.sh 报 登录中转失败 |
.env 的 RELAY_ADMIN_PASSWORD 与中转里已有管理员的口令不一致(改过口令会这样)。清掉 relay-data 卷重新初始化,或改回原口令。 |
scripts/relay-init.sh 报 生成 PAT 失败 |
docker compose logs relay 看中转自己的报错。 |
| 存入草稿箱报 IP 白名单错误 / 提示白名单未生效 | 白名单生效有等待期,等几分钟再试;同时确认填的是实际调用微信的那个中转的出口公网 IP(模式 A 下就是这台服务器的公网 IP)。 |
| 存入草稿箱报 401 | 中转 PAT 不对、已过期,或 scope 里没有 wx。 |
| 存入草稿箱报封面字体 hash 不符 / 封面渲染失败、字体不可用 | 封面渲染依赖固定的 Noto CJK 字体文件,字节必须与封面合同一致;镜像内已固定安装,自建镜像时请勿改 Dockerfile。 |
| 正文里的外链图片不显示 | Worker 在发布时要能访问那个图片地址(确认服务器能出网);更省事的是在编辑器里上传图片,走应用自己的存储。 |
| 草稿箱里出现重复文章 | 中转按 Idempotency-Key 去重:同一把 PAT 下同 key 的重复请求会返回首次结果。出现重复多半是换了不同的 PAT 或不同部署,请先确认草稿箱实际结果,不要重复发布。 |
| 连接列表提示「这个连接没有中转访问令牌(PAT)」 | 部署没有统一持有中转令牌,或之前持有过、后来被撤掉了。点「更新密钥」给这个连接补一个 PAT。 |
默认仍然是 bootstrap 单用户:一个访问口令管一个部署。要多人共用一个实例时,可以把登录换成任意标准 OIDC 服务(Keycloak、Authentik、Zitadel、Authelia、Cloudflare Access 等),只填环境变量,不需要改代码。
OIDC 配置说明
开启方式是把 PUBLISHING_AUTH_MODE 改成 oauth,并按下面三种端点来源之一配好:
| 端点来源 | 什么时候用 | 需要填什么 | 行为 |
|---|---|---|---|
| 标准发现(推荐) | 你的 IdP 提供 /.well-known/openid-configuration |
PUBLISHING_OAUTH_ISSUER + client id/secret + redirect |
启动时读一次发现文档,取 authorization_endpoint/token_endpoint/userinfo_endpoint。发现失败就是启动失败,不会悄悄换到别的端点 |
| 显式端点 | 你的 IdP 不提供发现文档,或不想让服务启动时访问外网 | 三个 *_ENDPOINT 变量 + client id/secret + redirect |
完全按你填的地址调用,不发任何发现请求。三个端点必须写全,缺一个就启动失败 |
| 既有形态(默认) | 保持原有的私有授权服务形态 | PUBLISHING_OAUTH_PORTAL_URL(固定 /oauth/authorize)+ PUBLISHING_OAUTH_BASE_URL + client id/secret + redirect |
与改动前完全一致,现有部署不需要改任何配置 |
Keycloak / Authentik 风格的配置示例(全部用占位地址):
PUBLISHING_AUTH_MODE=oauth
# 方式一:标准发现(只填 issuer,端点自动获取)
PUBLISHING_OAUTH_ISSUER=https://idp.example.com/realms/main
PUBLISHING_OAUTH_CLIENT_ID=REGISTERED_CLIENT_ID
PUBLISHING_OAUTH_CLIENT_SECRET=SERVER_ONLY_SECRET
PUBLISHING_OAUTH_REDIRECT_URI=https://studio.example.com/api/auth/oauth/callback
# 方式二:显式端点(三个都要填;不用时可以整段删掉)
# PUBLISHING_OAUTH_AUTHORIZATION_ENDPOINT=https://idp.example.com/realms/main/protocol/openid-connect/auth
# PUBLISHING_OAUTH_TOKEN_ENDPOINT=https://idp.example.com/realms/main/protocol/openid-connect/token
# PUBLISHING_OAUTH_USERINFO_ENDPOINT=https://idp.example.com/realms/main/protocol/openid-connect/userinfo
# 发现开关:auto(默认,只给 issuer 时用发现)/ 1(强制发现)/ 0(永不访问外网)
# PUBLISHING_OAUTH_DISCOVERY=auto要点:
- issuer 可以带路径(如
/realms/main),但必须是精确受信值:HTTPS(或显式回环 HTTP 用于本地调试),不带 query/fragment,尾部斜杠会被规范化。issuer 带路径时只允许在标准发现与显式端点两种方式下使用。 - redirect 必须精确等于
${APP_ORIGIN}/api/auth/oauth/callback,并与 IdP 里登记的回调地址一字不差。 - 端点地址不带 query、fragment 或凭据,也不能用非回环的明文 HTTP。
- 令牌交换用标准表单(
grant_type=authorization_code等),用户信息用Authorization: Bearer <access_token>。只支持「客户端密钥放在请求体」这一种客户端认证方式;要求client_secret_basic的 IdP 需要换一种方式接入。 - 会话策略仍是本项目自己的:上游令牌最长可到一天,但登录会话固定 7 天,不随上游令牌变长。
- 成员要管理员先绑定:OIDC 登录的用户不会自动成为成员。用受控命令按
issuer/subject绑定(subject就是 IdP 的sub,标准 OIDC 里它不一定是 UUID,大小写按原样保留)。PUBLISHING_SELF_PROVISION的会员自动开通是托管部署专属,开源默认关闭。 - 发现请求只在启动时发生一次;不想让服务启动时依赖外网,就用显式端点,或把
PUBLISHING_OAUTH_DISCOVERY设成0。
PUBLISHING_COPY_MODE 说明
客户可见文案按部署形态分流,由环境变量 PUBLISHING_COPY_MODE 控制:
neutral(默认):开源自托管用的中性文案,不出现任何私有品牌或会员体系用语。hosted:线上托管部署用的原有文案(含原来的续期指引与顾问说法)。
线上托管部署要保持原有文案,只需在部署环境里加 PUBLISHING_COPY_MODE=hosted。改这一项同样需要重启容器。注意:app 与前端由同一个镜像提供,必须整体同版本上线,不要只换前端。
兔狲由下面几个独立开源的项目组成,也都可以单独使用:
- bun-wx-server:微信公众号 API 中转,持有账号与令牌,代理 7 个微信草稿 / 素材接口,即这里的
relay服务。 - zzhub-pipeline:发布流水线 CLI 与云端 Worker(npm 包
@akairos/pipeline),即这里的worker服务。 - myno:基于 Milkdown 的 Markdown 编辑器 / 渲染器(
@akairos/myno),用于文章正文。 - typeset-studio:封面与排版渲染器(
@akairos/typeset-studio),用于文章封面。
按上面的步骤多数情况下可以自己跑通。如果卡住了,或者想让我帮你部署、接入公众号、做定制,欢迎联系:
- 作者微信:
akairos,部署、接入与使用问题都可以问。 - 问题反馈:请优先提交 issue,附上版本号、部署方式与现象描述。
- 讨论区:Discussions,安装与配置类问题可以在那里问;开头的「开始之前」贴里有最省事的部署路径。
bun install --frozen-lockfile
bun run check # tsc --noEmit
bun run build # vite build测试需要两个环境变量,缺一不可:
TEST_DATABASE_URL:必须指向专用的*_test数据库,不得指向正式业务库(测试会清理目标库的应用表)。TYPESET_ARTICLE_COVER_FONT_PATH:封面字体文件路径,字节必须与封面合同一致(容器内是/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc;宿主可用apt install fonts-noto-cjk装同款)。
TEST_DATABASE_URL=postgres://USER:PASS@127.0.0.1:5432/wxstudio_test \
TYPESET_ARTICLE_COVER_FONT_PATH=/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc \
bun run test- 贡献指南:AGENTS.md
- 部署细节:自托管说明
- 公开发布口径与发版流程(含开源前的清理说明):开源发布说明
- 第三方许可清单:THIRD_PARTY_NOTICES.md
本项目以 MIT 许可发布,Copyright (c) 2026 Kairos (k7airos)。第三方许可见 THIRD_PARTY_NOTICES.md。