【Bug已解决】Codex app upgrade loses Superpowers plugin installed state 解决方案
·
【Bug已解决】Codex app upgrade loses Superpowers plugin installed state 解决方案
原始报错线索:Codex app upgrade loses Superpowers plugin installed state(应用升级之后,原本装好的 Superpowers 插件状态丢失了,升级完插件像是没装过)。
一、现象长什么样
用户装好了一个插件(如 Superpowers),用得好好的;某天应用提示升级,点完升级:
- 插件不见了,功能消失;
- 设置里「已安装插件」列表空了;
- 重装插件又能用,但升级又会再丢;
- 排查发现:状态文件要么被新版本覆盖,要么存在「应用包内部目录」(升级即清)。 根因是插件状态存错了地方 / 升级流程没做状态迁移,更新把用户状态一并抹掉了。
二、背景:状态该存在哪
应用有两类存储位置,绝不能混:
| 位置 | 用途 | 升级时 |
|---|---|---|
| 应用包内(bundle / Program Files / /usr/lib) | 程序代码、默认资源 | 会被整体替换 |
| 用户数据目录(AppData / Library/Application Support / .config / .local/share) | 用户状态、插件、配置 | 应保留 |
| 插件安装状态是用户状态,必须存到用户数据目录,而不是应用包内。应用升级只替换包内文件,用户目录不动,状态才保得住。 |
三、为什么升级后丢失:根因
3.1 状态存进应用包目录
插件状态写在 Applications/App.app/Contents/... 或 C:\Program Files\App\plugins.json,升级整体覆盖 → 没了。
3.2 升级流程不迁移
新版本换了状态文件格式/路径,但升级程序没把旧状态搬到新位置,直接用了空的新文件。
3.3 状态无版本号
状态文件没记录「格式版本」,新版本读到旧格式要么忽略要么覆盖,无法识别需迁移。
3.4 原子性缺失
升级中途崩溃,状态文件写了半截 → 损坏 → 应用当「无状态」处理。
四、最小可运行复现(状态存错位置被覆盖)
下面演示「状态写在会被覆盖的目录」为何升级即丢:
import os, json
# 错误:把状态写进『应用包内目录』,升级会整体替换
APP_BUNDLE_DIR = "/Applications/App.app/Contents/Resources/state"
def save_plugin_state_bad(installed):
os.makedirs(APP_BUNDLE_DIR, exist_ok=True)
with open(os.path.join(APP_BUNDLE_DIR, "plugins.json"), "w") as f:
json.dump({"installed": installed}, f)
def simulate_upgrade():
# 升级:整个包目录被新版本覆盖
import shutil
shutil.rmtree(APP_BUNDLE_DIR, ignore_errors=True)
# 新版本包里没有用户旧状态 -> 丢失
if __name__ == "__main__":
save_plugin_state_bad(["Superpowers"])
simulate_upgrade()
path = os.path.join(APP_BUNDLE_DIR, "plugins.json")
print("升级后状态还在?", os.path.exists(path)) # False -> 丢失
状态写在包内,升级一覆盖就没了。
五、解决方案一:状态存用户数据目录(不被升级碰)
import os, sys, json
def user_state_dir(app_name="App"):
"""用户级状态目录:升级只换包,不碰这里。"""
if sys.platform == "darwin":
base = os.path.expanduser("~/Library/Application Support")
elif sys.platform == "win32":
base = os.environ.get("APPDATA")
else:
base = os.environ.get("XDG_CONFIG_HOME") or os.path.expanduser("~/.config")
p = os.path.join(base, app_name)
os.makedirs(p, exist_ok=True)
return p
def save_plugin_state(installed):
d = user_state_dir()
with open(os.path.join(d, "plugins.json"), "w") as f:
json.dump({"version": 1, "installed": installed}, f)
def load_plugin_state():
d = user_state_dir()
p = os.path.join(d, "plugins.json")
if not os.path.exists(p):
return {"version": 1, "installed": []}
with open(p) as f:
return json.load(f)
if __name__ == "__main__":
save_plugin_state(["Superpowers"])
# 模拟升级:只换包目录,用户目录不动
print("升级后状态:", load_plugin_state()) # 仍在 ✅
状态进用户目录,升级整体替换包时它安然无恙。
六、解决方案二:状态带版本号 + 升级迁移
状态文件记录 version,升级时若版本旧则迁移到新格式,绝不丢弃:
def migrate_state(old):
"""把旧版本状态迁移到当前版本,保留已安装插件。"""
current = {"version": 2, "installed": [], "enabled": {}}
# 旧版只有 installed 列表,迁移时保留并补 enabled 默认
current["installed"] = old.get("installed", [])
for name in current["installed"]:
current["enabled"][name] = True
return current
def load_with_migration():
state = load_plugin_state()
if state.get("version", 1) < 2:
state = migrate_state(state)
# 迁移后写回(幂等)
d = user_state_dir()
with open(os.path.join(d, "plugins.json"), "w") as f:
json.dump(state, f)
return state
if __name__ == "__main__":
# 假设旧状态是 version 1
save_plugin_state(["Superpowers"]) # 写入 version 1
# 升级后加载:自动迁移到 version 2,插件保留
print(load_with_migration())
version + migrate 保证升级不丢数据,旧格式平滑过渡(解决 3.2/3.3)。
七、解决方案三:原子写 + 升级保留验证
状态写入用临时文件 + 原子替换,且升级完成后做「状态保留校验」:
import os
def atomic_save(path, data):
tmp = path + ".tmp"
with open(tmp, "w") as f:
json.dump(data, f)
os.replace(tmp, path) # 原子,避免半截文件
def verify_after_upgrade(expected_installed):
"""升级后检查状态是否保留,缺失则告警/从备份恢复。"""
state = load_plugin_state()
missing = [p for p in expected_installed if p not in state["installed"]]
if missing:
print(f"[升级] 警告:以下插件状态疑似丢失: {missing}")
return False
return True
if __name__ == "__main__":
save_plugin_state(["Superpowers"])
atomic_save(os.path.join(user_state_dir(), "plugins.json"),
load_plugin_state())
verify_after_upgrade(["Superpowers"]) # True
原子写防损坏,升级后校验防静默丢失(解决 3.4)。
八、跨平台与发布要点
- 绝不写包内:任何用户状态都走用户数据目录;
- 备份:升级前备份状态文件,失败可回滚;
- 多版本共存:若支持多版本并行,状态按版本或向前兼容设计;
- 权限:用户目录权限要对,别因 ACL 写不进;
- 可观测:升级日志打印「保留/迁移了哪些插件」。
九、排查清单
「升级后丢插件状态」,按下面排查:
- 状态存哪了?是否在应用包内(升级会覆盖)(第五节);
- 升级是否迁移状态?旧格式是否被搬到新位置(第六节);
- 状态有版本号吗?新版本能否识别旧格式(第六节);
- 写入原子吗?半截文件会损坏(第七节);
- 升级后校验吗?丢失能否告警(第七节);
- 有备份吗?升级前是否备份可回滚;
- 权限够吗?用户目录是否写不进;
- 日志是否记录保留/迁移。
十、小结
「应用升级后丢失插件安装状态」的根因是插件状态存进了会被升级覆盖的位置,且升级流程不做状态迁移。通用修复:
- 存对地方:状态进用户数据目录(AppData / Application Support / .config),升级只换包不碰它;
- 带版本 + 迁移:状态文件记
version,升级时旧格式自动迁移保留(第六节); - 原子写 + 校验:临时文件 +
os.replace防损坏,升级后校验保留(第七节); - 备份可回滚:升级前备份状态,失败可恢复。 一句话:用户状态永远不能住在「应用包」里,它必须落在升级碰不到的用户目录,且每次升级都要把旧状态迁移到新格式而非丢弃。把状态当成「比代码更持久的东西」来对待,升级就再也不会让用户「装好的插件一夜消失」——这与第 116 篇路径规范、第 123 篇版本锁定、第 130 篇回退,共同体现「用户状态必须持久、可迁移、不被升级破坏」。

更多推荐


所有评论(0)