mirror of
https://github.com/SMNETSTUDIO/WeChat-AI.git
synced 2026-08-13 06:33:43 +08:00
146 lines
7.5 KiB
Markdown
146 lines
7.5 KiB
Markdown
# 运行时配置(管理面板)
|
||
|
||
`/admin` → **设置** 页可以改绝大多数原本只能写在 `.env` 里的配置。
|
||
|
||
## 优先级
|
||
|
||
```
|
||
.env(进程启动时读一次) ← 默认值
|
||
↓ 被覆盖
|
||
wa:settings:runtime(Redis JSON) ← 管理面板写入
|
||
↓
|
||
生效配置(进程内的同一个 cfg 对象)
|
||
```
|
||
|
||
- Redis 里**没有**某一项时,就用 `.env` 的值。
|
||
- 面板里把某项改回 `.env` 默认值,会**删除**该覆盖 —— 以后再改 `.env` 又能生效。
|
||
- 「全部恢复默认」删除整份 Redis 覆盖文档。
|
||
|
||
## 传播
|
||
|
||
每个节点每 **5 秒** 读一次 `wa:settings:runtime`,有变化才写入本进程配置并推给各服务。
|
||
所以:改动在**本节点立即生效**,其他节点**最多 5 秒**。没有用 pub/sub —— 一次 GET
|
||
相对请求路径上的 Redis 流量可以忽略,也省掉一条订阅连接。
|
||
|
||
## 范围
|
||
|
||
**不可配置(env-only)**,因为它们要么在能读 Redis 之前就得正确,要么改了会破坏已有数据:
|
||
|
||
| 变量 | 原因 |
|
||
|------|------|
|
||
| `REDIS_URL` | 鸡生蛋:覆盖本身存在 Redis 里 |
|
||
| `LLM_BASE_URL` / `LLM_API_KEY` / `LLM_MODEL` | 平台模型凭证 |
|
||
| `LLM_PROVIDER_SECRET` | 改了之后所有用户已加密的自定义模型 key 全部解不开 |
|
||
| `WECHAT_AI_TOKEN` / `LINUXDO_ADMIN_IDS` | 权限根,改错会把自己锁在外面 |
|
||
| `SESSION_COOKIE_NAME` / `COOKIE_SECURE` | 改了当场踢掉所有会话 |
|
||
| `PUBLIC_BASE_URL` / `CORS_ORIGINS` | 站点自身 URL 与跨域白名单 |
|
||
| `WECHAT_AI_HOST` / `WECHAT_AI_PORT` | 监听端口在 `listen()` 时固定 |
|
||
|
||
其余全部在面板里,包括 `TOOLS_BASE_URL` / `TOOLS_API_KEY`(密钥在 UI 里只显示掩码)。
|
||
|
||
`apps/api/src/runtime-config.test.ts` 有一条守卫用例:**任何新增的 AppConfig 字段**要么进
|
||
`SETTING_SPECS`,要么进那份 env-only 名单,否则测试直接失败。
|
||
|
||
`DEFAULT_PERSONA_SLUG` 刻意不在面板里:`cfg.defaultPersonaSlug` 在代码里没有任何消费方,
|
||
默认人设走的是数据库 `is_default`(管理台「人设」→ 设默认)。放进面板只会得到一个
|
||
「保存成功但什么都没发生」的控件。
|
||
|
||
## 需重启的项
|
||
|
||
绝大多数配置是热生效的。以下几项写入 Redis 后会打上橙色「需重启」徽章:
|
||
|
||
| 项 | 为什么 |
|
||
|----|--------|
|
||
| `WORKER_ENABLED` | `worker.start()` 只在启动时跑一次 |
|
||
| `REPLY_CONCURRENCY` | 消费者池在 `start()` 一次性拉起,没有单消费者取消机制,缩容做不到 |
|
||
| `LOG_LEVEL` | Fastify 创建实例时固化 logger |
|
||
| `STICKER_MAX_BYTES` | 路由注册时固化 `bodyLimit` |
|
||
|
||
## 热生效是怎么做到的
|
||
|
||
`cfg` 对象**按引用**传给 `registerRoutes` 和各个服务,所以:
|
||
|
||
- **路由**:87 处 `ctx.cfg.*` 是每请求读的,原地改 `cfg` 就够了,零改动。
|
||
- **在构造时快照选项的服务**:由 `runtime-config-apply.ts` 显式推送 ——
|
||
`ChatService` / `TryChatService` / `BotWorkerManager` / `ActivityBus` 各有一个
|
||
`applyRuntimeOptions`(或 `applyRuntimeConfig`)。
|
||
|
||
几个需要特殊处理、否则会静默失效的地方:
|
||
|
||
- `ChatService.webSearch` 原本在构造函数里判断一次,`WEB_SEARCH_ENABLED=false` 启动就永远
|
||
是 `null`。现在按 tools 配置的指纹惰性重建。
|
||
- `BotWorkerManager` 的 10 个 `readonly` 标量改成可写,且 setter 里复刻了构造函数的
|
||
clamp —— 面板不能写进一个构造函数本来会拒绝的值(比如 `leaseRenewSec=0`)。
|
||
- `leaseRenewSec` 还决定 `setInterval` 周期,改动时重新装载定时器。
|
||
- `ProactiveScheduler.globalEnabled` 被 `start()` 闩住(早退且留下 `stopped=true`),
|
||
false→true 必须重新进 `start()`,光改标志没用。
|
||
- `setRedisCommandHook` 现在无条件安装,否则 `DATA_STREAM_ENABLED` 这个开关永远是死的。
|
||
|
||
## API
|
||
|
||
**整个页面(含只读)限超管。** 超管 = 仍是管理员的用户里 `created_at` 最早那位。
|
||
|
||
| Method | Path | 权限 | 说明 |
|
||
|--------|------|------|------|
|
||
| GET | `/api/v1/admin/settings/runtime` | **超管** | 分组 + 全部项(当前值 / env 默认值 / 是否已覆盖 / 是否需重启)+ 交叉校验警告 |
|
||
| PATCH | `/api/v1/admin/settings/runtime` | **超管** | `{ patch: {key: value}, reset: [key] }` |
|
||
| POST | `/api/v1/admin/settings/runtime/reset` | **超管** | 删除全部覆盖 |
|
||
|
||
连读也限超管:这个面板能拿到 tools 网关密钥,能整个集群关掉 worker,
|
||
payload 本身也把每一个调参项和当前生效值列了出来。普通管理员访问返回
|
||
403 `super_admin_required`。
|
||
|
||
前端跟着服务端一起收口,和「节点 / 广播 / 数据流」用同一套三处门禁:
|
||
`switchTab` 拦截并回落到「系统」页、`boot()` 隐藏侧栏按钮与 `<section>`、
|
||
命令面板(Ctrl/⌘K)过滤掉该条目。
|
||
|
||
两个写操作都会记审计(`runtime_settings_updated` / `runtime_settings_reset`),
|
||
含变更的 key 列表与需重启的 key。
|
||
|
||
密钥字段(`type: "secret"`)的约定:
|
||
|
||
- GET 只返回掩码 `••••••••`,永远不回真值
|
||
- PATCH 传空字符串 = **不修改**
|
||
- PATCH 传 `-` = **清空**
|
||
- 把掩码原样回传会被忽略,不会变成字面值
|
||
|
||
## 交叉校验
|
||
|
||
保存时会返回警告(只提示,不改用户填的值):租约 TTL ≤ 续约间隔、延迟上下限倒挂、
|
||
`memoryFullInjectMax < memoryTopK`、开了联网但没配 tools 网关、配了网关但密钥为空、
|
||
二次过滤与模型直出 JSON 同时开启。
|
||
|
||
## Redis 读失败时的行为
|
||
|
||
「读不到」和「不存在」必须分开——把两者都当成「没有覆盖」会让一次网络抖动
|
||
把整个节点悄悄退回 `.env`(此前被面板收紧的开关会瞬间放开)。所以:
|
||
|
||
- **轮询读失败** → 保持上一份已知配置不动,打一条日志,`refresh()` 返回 false。
|
||
- **写路径读失败** → 直接拒绝,返回 **503**。绝不能基于失败的读做合并再写回,
|
||
那会把其他所有覆盖一起抹掉。
|
||
- **例外:全部恢复默认**。它本来就要丢弃旧文档,所以读不到时也允许执行 ——
|
||
这是文档损坏时唯一的产品内自救路径。
|
||
|
||
## 取值受限的项
|
||
|
||
`SettingSpec.options` 声明闭集,面板渲染成下拉框,服务端对不在集合里的值直接拒绝
|
||
(不做替换,免得把拼写错误藏起来)。目前只有 `LOG_LEVEL` 用到:pino 遇到未知级别会在
|
||
Fastify 建实例时抛错,而这一项又是「需重启」,一个拼写错误会在下次重启时让每个节点
|
||
反复崩溃,而且管理接口起不来、改不回去。
|
||
|
||
## 并发写
|
||
|
||
`patch()` 是读-改-写:读 Redis 当前文档 → 合并 → 整份写回。合并是**按文档**而不是按字段的,
|
||
所以两个超管在两个节点同时保存,后写的那份会带着自己的改动覆盖掉前一份的全部改动。
|
||
|
||
为此写路径上加了一把短锁 `wa:settings:runtime:lock`(`SET NX EX 5`,最多重试 5 次 ×
|
||
120ms),把整个集群的读-改-写串起来。锁只在写时用,读路径完全不碰它。
|
||
拿不到锁时不阻塞管理员,仍然继续写 —— 锁是降低窗口,不是强一致保证;写操作是超管专属
|
||
且极低频,这个取舍是刻意的。
|
||
|
||
## 多节点注意
|
||
|
||
`MAX_BOTS_PER_WORKER`、`LEASE_TTL_SEC`、`REBALANCE_SLACK` 参与的是**集群共享**的租约/再平衡
|
||
协议。因为覆盖存在 Redis、所有节点读同一份,各节点最终看到的是同一组值 —— 但在 5 秒收敛
|
||
窗口内可能短暂不一致,表现为一次多余的再平衡。改这几项建议避开高峰。
|