# 腾讯会议企业管理后台 -> 应用管理 -> 企业自建应用 -> 应用凭证:App ID TM_APP_ID= # 腾讯会议企业管理后台 -> 应用管理 -> 企业自建应用 -> 应用凭证:SDK ID TM_SDK_ID= # 腾讯会议企业管理后台 -> 应用管理 -> 企业自建应用 -> 应用凭证:Secret ID TM_SECRET_ID= # 腾讯会议企业管理后台 -> 应用管理 -> 企业自建应用 -> 应用凭证:Secret Key TM_SECRET_KEY= # 腾讯会议企业管理后台 -> 成员管理:作为接口调用方的腾讯会议管理员 userid(operator-id) TM_OPERATOR_ID= # 腾讯会议开放平台 API 网关地址,默认 https://api.meeting.qq.com,一般无需修改,留空则使用默认值 TM_BASE_URL= # 调用腾讯会议 API 的限流值(QPS),默认 5,留空或不写则使用默认值。 # 必须是 >= 1 的整数:填 0 会让令牌桶永远补不满,所有腾讯 API 调用静默卡死, # 故 loadConfig 在启动期强制校验。 TM_QPS= # --------------------------------------------------------------------------- # 企业微信自建应用(可选,三项要么全填、要么全不填) # # 只服务于「真人企微扫码登录」(设备授权流程)。三项全不填 = 本次部署不启用 # 扫码登录:/api/v1/auth/device/code、/device、/auth/wecom/callback 等路由统一 # 返回 501 wecom_not_configured,preflight 第 5 项判 skip(不是 fail),客户端 # 改用服务账号认证(POST /api/v1/auth/service-token)。 # # 只填一部分会在启动期报错——那几乎一定是打错变量名或漏配,而不是想停用企微。 # 详见 docs/deploy.md §4。 # --------------------------------------------------------------------------- # 企业微信管理后台 -> 应用管理 -> 自建应用:企业 ID(企业信息页可查看) WECOM_CORP_ID= # 企业微信管理后台 -> 应用管理 -> 自建应用:AgentId WECOM_AGENT_ID= # 企业微信管理后台 -> 应用管理 -> 自建应用:Secret WECOM_SECRET= # 网关自身使用的 MySQL 连接串,由部署环境提供,格式:mysql://user:pass@host:3306/db?charset=utf8mb4 DATABASE_URL= # 网关签发/校验用户会话 JWT 的密钥,必须 >= 32 位随机字符串(例如 openssl rand -base64 32)。 JWT_SECRET= # 网关自身对外可访问的 Base URL,由部署环境提供(企微跳转等会用到) GATEWAY_BASE_URL= # 身份映射策略,由部署方根据企业内 腾讯会议 userid 与 企微 userid 的对应关系选择:direct | email | table IDENTITY_STRATEGY= # 可选,默认 1。必须精确等于网关前方会追加 X-Forwarded-For 的可信代理层数(例如仅 # 一层 Nginx/SLB 做 TLS 终止则填 1);网关取 XFF 右数第 N 段作为客户端 IP 用于登录 # 限流。配错的两个方向后果不同—— # 填多了(大于实际层数):下标会落进客户端可在请求头里自行伪造的 XFF 前缀段, # 登录限流可被绕过(安全风险)。 # 填少了(小于实际层数):下标会落在更内层代理的固定 IP 上,不同客户端被合并 # 进同一个限流桶(误伤,可用性问题,不是绕过)。 # 不确定时宁可偏小,不要偏大。 TRUSTED_PROXY_HOPS= # --------------------------------------------------------------------------- # 归档 worker(bun run worker)。 # # 其中 MDE_ARCHIVE_ROOT / MDE_NAS_ROOT **网关进程也会读**(阶段 4 · T8):控制台 # 的「归档存储」页要报 NAS 连通与容量(走 src/worker/nas-probe.ts),「立即清理 # 已到期文件」要真的删本地文件。网关这边**不做启动期强校验**——其余功能与挂载无关, # 为一张页面拒绝启动是过度反应;没配的后果显式降级且都看得见:NAS 那块显示"不可达", # 清理端点返回 503 并说清是挂载/配置问题(不是"没有可清理的文件")。 # 也就是说:只跑 worker 不跑控制台时,这两项仍然只有 worker 用; # 要用控制台的归档存储页,网关进程得挂到**同样的两个路径**上。 # 下面 MDE_WORKER_* 那几项则仍然只有 worker 读。 # --------------------------------------------------------------------------- # 服务端归档区的根目录,worker 把拉回来的资产按 # ///<日期_时分_主题_会议号>/<资产文件名> 落盘。 # # 必填,且 worker 启动时会校验它【已经存在、是目录、且真的可写】(写一个探针文件再删掉)。 # worker 【不会】替你把这个目录建出来:那样的话把路径打错一个字母(比如 /mnt/archiv) # 就会静默造出一棵新目录树、把整场会议归档进去、退出码 0,而本系统是「NAS 主存储、 # 本地 30 天后删」,等于一个月后永久丢失。归档根目录在部署上是挂载点,本就该先存在。 # # 探针能证明「只读挂载 / 权限错配」,【不能】证明磁盘还有空间:它写的是空文件, # 盘满了也只要一个 inode 就能建出来。磁盘空间是下载时逐个资产判的(判失败记 skipped)。 # 每次 fs 调用带 5s 超时:硬挂载的 NAS 掉线时 stat/write 是【挂住】而不是报错, # 没有超时的话这道启动校验自己就变成一次无限期的静默挂起。 # # 【本地开发也不要指到 /tmp 下】。macOS 与多数发行版会清理 /tmp,重启一次这棵树 # 就没了——而库里仍然记着那些资产「已下载完、已归档」。两边对不上之后,界面上每 # 一场都显示已完成而文件并不存在,且不会有任何报错;等到期清理来删的时候才会因为 # 校验不过而卡住。真实发生过一次,代价是 1178 个资产记录与 172 场归档记录作废 # (用 scripts/reset-meeting-data.ts 清库重来)。挑一个重启还在的位置, # 比如 ~/mde-data/local。 MDE_ARCHIVE_ROOT= # 归档流水线(P2)的搬运目的地:NAS 挂载点根目录。worker 把 MDE_ARCHIVE_ROOT 下已经 # 下载完成的资产复制过来,落盘并双端校验哈希(本地/NAS 各算一遍 sha256,一致才记为 # 归档成功)。 # # 目录布局由**归档规则栈**决定(控制台 §4.6),不再是固定的 //。 # 规则的 effect 是一段目录模板,**相对本项**解释:`meetings/{年}/{月}/`。 # 绝对路径与 `..` 一律判为不归档(写到挂载点外面去,sidecar 也会跟着跑出去)。 # # 目录名自 2026-09-08 起不含会议主题(会议目录是 <日期>_<时分>_<会议号>);`{标题}` # 占位符仍可用,但会把中文带进 NAS 路径,跨系统挂载时编码不稳,不推荐。存量目录用 # `bun scripts/rename-archive-dirs.ts`(默认 dry-run,加 --apply 才动)改成新格式。 # # **这一项选在哪一层,决定了模板能表达什么。** 想让不同部门的会议进 /nas/meetings 与 # /nas/meetings-finance 两棵树,本项就得设成 /nas,模板写 `meetings/…` 与 # `meetings-finance/…`;设成 /nas/meetings 的话后者只能写成 `../meetings-finance/`, # 而那是被拒的。**改本项不会让已归档的文件失联**——nas_path 与 nas_dir 都存在库里, # 到期清理读的是记录不是重算。 # # 必填,worker 启动时只做「非空」这一层最小校验——不做 MDE_ARCHIVE_ROOT 那一整套 # 存在性/目录/可写性 + 超时探测。那一整套运行期可重复调用的版本是 probeNas # (src/worker/nas-probe.ts),服务的是控制台"归档存储"页的连通状态展示, # 本项只负责让归档流水线知道该往哪个目录搬。打错路径或指向本地磁盘而非真的 NAS # 挂载点,后果与 MDE_ARCHIVE_ROOT 打错一样:静默把数据搬进错误的地方。 MDE_NAS_ROOT= # 可选,默认 4。worker 同时下载的资产数。 # 上限是数据库连接池给的:领取任务的 claimNext 在事务期间独占一条连接, # 每个执行体在下载期间还压着一条未 await 的进度回写,所以要求 并发度 × 2 <= 10(池上限), # 即最大 5。默认取 4 是刻意站在上限之下,给发现阶段与收尾写库留两条连接。 # 配过头会在启动期直接报错——mysql2 没有取连接超时,池耗尽在运行期兜不住, # 表现是进程无限期静默挂起,只能挡在启动期。 MDE_WORKER_CONCURRENCY= # ── 定时任务调度器(阶段 4 · T11,A4)· `bun run scheduler` ──────────────────── # # spec.md §4.8 的四个任务(拉取新录制 / 归档到 NAS / 清理到期文件 / 刷新采集清单) # 由一个**常驻**进程执行,与一次性的 `bun run worker` 分开: # # bun run worker 跑一轮就退,按退出码说话,运维手动补跑用(--from/--to) # bun run scheduler 常驻,四个任务按各自的频率跑 # # ⚠️ **调度器只能有一份,且绝不能跑在网关进程里。** 网关是多实例的,四个任务各跑 # N 份意味着 N 个实例同时往同一个 NAS 目录搬同一批文件、同时对同一批本地文件执行 # 不可逆删除。它读上面 MDE_ARCHIVE_ROOT / MDE_NAS_ROOT 与全部腾讯凭据。 # # **下载执行体在任务一里**(阶段 4 · T14):任务一 = 发现 + 入队 + 执行下载队列, # 与 `bun run worker` 共用同一段代码(src/worker/index.ts 的 runFetchRound)。 # 也就是说**跑起调度器,录制就会被拉下来**,`bun run worker` 只剩人工补跑一个用途 # (补一段停机窗口、按会议号点名重拉)。T11 时任务一只做前两件,后果是四个任务的 # 运行记录全绿而资产一直排在队列里没人取——这条缺口已经补上。 # # 归档仍然只有任务二一个入口:任务一那一轮**不含归档**,否则两个任务会同时往同一个 # NAS 目录搬同一批文件。 # 可选,默认 0(UTC)。**分钟**,东八区填 480。 # 它只影响「每天 03:00 清理到期文件」——那个 03:00 指的是运维眼里的凌晨三点。 # 留空按 UTC 走,于是清理会跑在北京时间上午 11 点的业务高峰上。 # **网关进程也读这一个变量**(控制台「下次运行」要显示同一个时刻),两处必须一致, # 配得不一样不会报任何错,只会让人在错误的时间点守着等清理。 MDE_SCHEDULER_TZ_OFFSET_MIN= # 可选,默认 30。调度器多久检查一次「有没有任务到点了」,秒。 # 必须明显小于最短的任务周期(5 分钟),否则会整片整片地错过。 # 它不是任务频率——tick 再密,一个时间片里也只会跑一次。 MDE_SCHEDULER_TICK_SEC= # 可选,默认 24。任务一(拉取新录制)每次往回看多久,小时。 # 每 15 分钟跑一次,只为发现刚结束的会议,所以窗口不需要大——大窗口意味着每轮都对 # 同一批老会议重新调一次有配额的腾讯接口。24 小时留的是短暂停机(重启、部署)的余量。 # **它不负责补跑长时间停机**:停了三天请用 `bun run worker --from … --to …` 手动补, # 让定时任务自己往回看三天等于每 15 分钟重扫一次三天的会议。 MDE_SCHEDULER_FETCH_LOOKBACK_HOURS= # 可选,默认 2。任务一同时下几个资产。 # **比 MDE_WORKER_CONCURRENCY 的默认值(4)低是刻意的**:一次性 worker 独占那个连接池, # 而调度器要把同一个池分给四个任务。任务一压着 并发度 × 2 条连接(领任务的事务连接 + # 同一执行体在途的那条进度回写),另外三个任务各自还要一条,池上限是 10。 # 取 2 时稳态峰值 2×2+3 = 7,留得下余量;取 4 就是 8+3 = 11,越线之后归档/清理/清单 # 会开始排队等连接,而 mysql2 没有取连接超时——排上队就是无限期地等。 # 硬上限因此是 3(启动期校验,配过头当场报错)。 MDE_SCHEDULER_FETCH_CONCURRENCY= # ── 控制台前端(阶段 6 · R6-b)───────────────────────────────────────────── # # 可选,默认 console/dist(相对网关进程的工作目录)。网关在启动时看这个目录里有没有 # index.html:有就把控制台页面挂在根路径(`/`、`/audit`、`/assets/...`),与 `/api` # 同源,会话 cookie 的 SameSite=Strict 才成立;没有则只提供 API,日志里提示一句, # **不拒绝启动**。构建产物由 `cd console && npm run build` 产出,部署时随代码一起同步。 MDE_CONSOLE_DIST= # ── Docker Compose(docker-compose.yml)────────────────────────────────────── # # `docker compose up -d --build` 一次起 MySQL + 网关 + 调度器。上面所有变量原样交给 # 容器;下面这几项只有 Compose 自己读,不进业务代码。两处例外要知道: # · MDE_ARCHIVE_ROOT / MDE_NAS_ROOT 在这里写的是**宿主机**目录,Compose 把它们挂到 # 容器内的 /data/archive 与 /data/nas,容器里看到的是后者。目录必须已存在且对 # 运行 uid(MDE_UID,默认 1000)可写。 # · DATABASE_URL 留空 = 用 Compose 自带的 MySQL(下面 MYSQL_* 那几项);填了 = 用外部库, # 容器里的 localhost 不是宿主机,本机的库要写 host.docker.internal。 # 网关映射到宿主机的端口,默认 3000。 MDE_GATEWAY_PORT= # 网关与调度器在容器里以哪个 uid:gid 运行,默认 1000:1000。Linux 上把它改成两个目录的属主。 MDE_UID= MDE_GID= # 自带 MySQL 的业务账号口令(用户名固定 mde,库名固定 meeting_gateway)与 root 口令。 # 默认值只适合本机试跑;库只绑 127.0.0.1,但还是请改掉。口令里含 @ : / % 要按 URL 规则转义。 MYSQL_PASSWORD= MYSQL_ROOT_PASSWORD= # 自带 MySQL 的数据目录:留空走 Docker 命名卷 mde-mysql-data,填宿主机绝对路径则落到那里。 MYSQL_DATA_DIR= # 自带 MySQL 映射到宿主机 127.0.0.1 的端口,默认 33306(避开本机已有的 3306)。 MYSQL_HOST_PORT= # 本文件放在仓库外时(例如 /etc/meeting-export-gateway/.env),启动写 # `docker compose --env-file <路径> up -d`,并把同一个路径填在这里——前者管 Compose # 插值,后者管交给容器的那一份。放在仓库根目录则留空。 MDE_ENV_FILE=