配置持久化与恢复
运行时设置保存在 data/config.json。core/config/persistence.py 是所有 JSON 配置写入口共用的持久化边界,Telegram、WebUI、频道调度和投票管理器都应通过该边界修改配置。
选择写入接口
update_config_fields(fields):只替换明确提供的顶层字段,适合/setaicfg等字段设置。每次操作都从最新完整配置读取,未知字段和其他业务设置保持原值。read_config()配合save_config_changes(snapshot):严格读取后修改并提交差异,适合频道、调度、投票、转发等包含增删操作的配置。字典中的显式删除会提交到磁盘;不同路径的并发修改会合并,同一字段或数组发生冲突时拒绝保存,调用方应重新读取。save_config(config):明确的完整替换接口,不执行合并。保留有意删除字段的语义,同时阻断三字段 AI 草稿覆盖和异常大量顶层字段删除。普通业务设置应使用前两种接口。load_config():保留兼容的只读容错行为。读取失败会返回空字典形态的无效快照,该结果不能提交修改。不要通过dict(snapshot)或.copy()丢弃写入基线。
WebUI 使用 deps.get_config() 和 deps.write_config(),保存成功后才安排热重载。不要重新引入直接 open(..., "w") 或 json.dump() 写入完整配置的路径。
保存与失败行为
事务使用线程锁和操作系统文件锁覆盖读取、候选校验、备份和替换。锁文件为 config.json.lock;运行期间不要删除或替换该文件。配置必须已存在、可解析且通过业务校验;首次安装仍应从示例配置创建完整文件。
写入使用同目录临时文件、flush()、fsync() 和 os.replace()。Windows 在同卷私有暂存子目录中写入,保留现有文件的 DACL 和继承保护,新备份禁止继承外层目录的宽松读取权限。读取失败、候选校验失败、备份失败或替换失败都会向调用方报错,不把空配置当作写入起点。持久化错误消息不包含配置值。
/setaicfg 只提交明确输入的 AI 字段;/skip 在提交时保留该字段的最新值,/cancel 不写文件。保存失败保留当前用户草稿,可重新发送 Model 重试或取消。API Key 的展示完全遮蔽,输入文本和异常原文不写入日志。
恢复版本与保留策略
config.json.last_valid保存可恢复的有效配置。启动不会覆盖已有且有效的该文件。config.json.backups/保存按 UTC 时间命名的独立 JSON 快照,文件内容不原地覆盖,最多保留最近 30 个版本。相同的连续快照不会因反复重启产生新版本。- 覆盖现有配置前必须成功保存有效恢复点。重载只将校验通过的版本纳入历史;坏 JSON 或异常大量字段丢失不会被固化为有效备份。
- 热重载验证失败会在同一文件锁内恢复已有有效快照;内存快照不可用时尝试历史版本。损坏的备份会跳过,不将其直接复制回配置文件。
- 备份包含实际凭据,新建历史目录和文件使用所有者权限,并已加入 Git 忽略列表。限制生产目录访问,不将备份上传到 Issue 或日志。
历史版本不是永久外部备份。仍应通过服务器备份系统保存重要设置;磁盘损坏或历史版本全部失效时,本地恢复机制不能重新生成已丢失的数据。
已发生字段丢失时
- 停止机器人及其他写配置的进程。将当前
config.json、config.json.last_valid、历史目录和相关日志复制到单独的受限事故目录,保留现场。 - 查找事故前的完整快照。先检查 JSON 可解析、顶层为对象,并确认频道、调度、投票、转发及未知扩展字段仍然存在。不要把凭据原文输出到终端或公开记录。
- 保留独立副本后,将选定的完整历史快照复制为
data/config.json,然后重启并检查业务设置及热重载状态。启动阶段发现异常大量字段删除会拒绝接受该文件,以保留现有恢复点。 - 对旧版
/setaicfg处理过的凭据,检查服务器历史日志中的原文输入记录;限制日志访问并轮换可能已经暴露的 API Key。修复不会远程清理服务器日志,也不会替换外部供应商的凭据。
如果事故发生在本修复之前,且当前配置、last_valid 和其他副本都只剩 AI 三字段,本修复无法找回原来的频道与调度值。需要从服务器快照、独立备份或原始配置记录恢复。