1. 容器重建后 Trae 插件全没了,问题到底出在哪
如果你用 Trae 通过 SSH 连进 Docker 容器做 Python 开发,大概率踩过这个坑:docker compose down之后重新up,SSH 照样连得上,终端照样能敲命令,但 Trae 左侧的扩展列表空了,之前装的 Ruff、Python、Pylance 全都不见。这不是 Trae 的 bug,也不是 SSH 配置坏了,而是容器内/home/dev/.trae-server/extensions这个目录本身就在容器可写层里,容器一删,插件跟着一起消失。
正常思路是提前把插件目录挂载到宿主机,或者用脚本在容器启动后从缓存目录恢复。原文给的方案是scripts/restore_trae_extensions.sh从/app/vsix_cache把插件拷回去。听起来很顺,但实际排障时你会发现:脚本明明执行了,echo也打印了「恢复完成」,可 Trae 重连后插件还是没回来。这时候问题往往不在脚本逻辑本身,而在三个地方——脚本里的SRC/DEST路径和docker-compose.yml的volumes挂载点对不上、cp因为目标目录有残留文件而中途报错退出、以及脚本执行时机不对导致 Trae 已经读完了扩展目录。
这篇就按排障视角走一遍:用 Codex 接入 TaoToken 通道,让模型帮你逐行核对restore_trae_extensions.sh的路径映射,检查cp命令的容错写法,最后在容器重建后一键把 Trae 插件还原回来。适合已经在用 Trae + Docker + SSH 这套组合、但被插件持久化卡住的 Python 开发者。
2. 前置:给 Codex 配好 TaoToken 通道
排障这件事,靠自己一行行比对 YAML 和 shell 变量很容易漏。我习惯让 Codex 先读一遍docker-compose.yml和restore_trae_extensions.sh,把两边的路径做交叉验证。但 Codex 要能稳定跑起来,得先给它一个可用的模型通道。
打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册后进控制台创建 Key。地址是 https://taotoken.net/api ,注意这个 Base URL 不带/v1,也不要额外拼 UTM 参数,直接原样填。Codex 侧配置里把 provider 的 base_url 指向它,api_key 填刚生成的 Key。
创建 Key 的入口在控制台的 API Keys 页面,模型对话入口可以用来先验证通道是否通。如果你后面要长期用 Codex 做编码和 Agent 任务,可以看下 Coding Plan 的额度说明,避免排障排到一半额度不够。
配置写完后,先用一句简单请求确认通道可用,再让它读项目文件。这一步别省,通道没通的话后面所有排查都是白费。
3. 可复制配置:让 Codex 核对路径映射
3.1 先确认 docker-compose.yml 的挂载点
排障第一步是把「宿主机目录」和「容器内目录」的对应关系列清楚。假设你的docker-compose.yml里是这样:
services: python-dev: build: context: . dockerfile: Dockerfile image: my-python-dev:latest container_name: python-dev-container ports: - "2222:22" volumes: - .:/app environment: SSH_PUBLIC_KEY: ${SSH_PUBLIC_KEY} DISPLAY: ${DISPLAY} restart: unless-stopped这里.:/app意味着宿主机的项目根目录(也就是D:\PythonProjects\)挂到了容器/app。那么vsix_cache在宿主机是D:\PythonProjects\vsix_cache,在容器里就是/app/vsix_cache。这个映射关系是后面所有路径判断的基准。
3.2 检查 restore 脚本的 SRC/DEST
原文的恢复脚本长这样:
#!/usr/bin/env bash set -euo pipefail SRC="/app/vsix_cache" DEST="/home/dev/.trae-server/extensions" echo "=== 开始恢复扩展 ===" mkdir -p "$DEST" find "$DEST" -mindepth 1 -maxdepth 1 -exec rm -rf {} + cp -av "$SRC/." "$DEST/" echo "=== 扩展恢复完成 ==="把这段和 compose 的 volumes 对照,SRC=/app/vsix_cache对应宿主机D:\PythonProjects\vsix_cache,没问题。DEST=/home/dev/.trae-server/extensions是 Trae 在容器内的扩展目录,也没问题。但实际排障中常见的坑是:有人把SRC写成了/app/vsix_cache/(带尾斜杠),或者DEST写成了/home/dev/.trae-server/extension(少个 s),脚本照样能跑完不报错,但插件就是没恢复。
让 Codex 帮你做这件事,可以直接把两个文件贴进去,用这样的提示:
读取 docker-compose.yml 的 volumes 配置,以及 scripts/restore_trae_extensions.sh 里的 SRC 和 DEST 变量。 逐项核对: 1. SRC 指向的容器路径,是否等于某个 volume 挂载点的子目录; 2. DEST 是否与 Trae 实际读取扩展的目录一致; 3. cp 命令在目标目录已有残留文件时是否会中断。 输出一份对照表,标出不一致的地方。3.3 cp 命令的容错写法
set -euo pipefail加上cp -av,一旦某个文件拷贝失败,整个脚本会立刻退出,后面的插件自然恢复不全。更稳的写法是先清空目标目录再拷,并且对cp的返回码做判断:
#!/usr/bin/env bash set -uo pipefail SRC="/app/vsix_cache" DEST="/home/dev/.trae-server/extensions" if [ ! -d "$SRC" ]; then echo "缓存目录不存在: $SRC" exit 1 fi mkdir -p "$DEST" find "$DEST" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true if cp -a "$SRC/." "$DEST/"; then echo "扩展恢复完成,共 $(ls -1 "$DEST" | wc -l) 项" else echo "拷贝过程中出现错误,请检查 $SRC 权限与磁盘空间" exit 2 fi注意这里去掉了-e,改成手动判断cp的返回码,这样即使个别文件拷贝失败,脚本也会把错误信息打出来,而不是静默退出。find ... -exec rm -rf后面加了|| true,避免目标目录为空时find返回非零导致脚本中断。
4. 验证请求:容器重建后跑通恢复流程
配置改完后,按这个顺序验证一遍。
先在 Trae 里装一个插件,比如 Ruff,然后执行备份:
bash scripts/backup_trae_extensions.sh备份脚本的逻辑是把/home/dev/.trae-server/extensions的内容拷到/app/vsix_cache。执行完在宿主机D:\PythonProjects\vsix_cache下应该能看到插件目录。
接着模拟容器重建:
docker compose down docker compose up -d --build等容器起来后,SSH 连进去,执行恢复:
bash scripts/restore_trae_extensions.sh如果输出是「扩展恢复完成,共 N 项」,并且 N 大于 0,说明拷贝成功。这时候在 Trae 里断开重连,或者按Ctrl+Shift+P执行Developer: Reload Window,左侧扩展列表应该能把 Ruff 找回来。
如果想让恢复自动化,可以在bootstrap-sshd.sh里加一段,容器启动时自动调用恢复脚本:
if [ -f /app/scripts/restore_trae_extensions.sh ]; then bash /app/scripts/restore_trae_extensions.sh || true fi这样每次docker compose up之后插件就自动回来了,不用手动再跑一遍。
5. 本篇常见错排查
5.1 脚本路径和 compose 挂载点不一致
最常见的表现是脚本执行成功但插件没回来。原因通常是SRC写成了宿主机路径(比如D:\PythonProjects\vsix_cache),但脚本是在容器内执行的,容器里根本没有D:这个盘符。记住:脚本里所有路径都必须是容器内视角的路径,宿主机路径只在 compose 的volumes里出现。
5.2 cp 因目标目录残留而中断
cp -a "$SRC/." "$DEST/"在目标目录已有同名文件时,默认会覆盖,但如果遇到权限问题或者符号链接循环,会报错退出。配合set -e就是直接终止。排查方法是先手动ls -la "$DEST"看有没有异常文件,再用cp -av单独跑一次看具体报什么错。
5.3 Trae 已缓存扩展列表
有时候插件文件确实拷回去了,但 Trae 界面没刷新。这是因为 Trae 的扩展宿主进程还持有旧的目录句柄。解决办法是断开 SSH 连接后重新连,或者在命令面板执行Developer: Reload Window。如果还不行,把 Trae 完全退出再打开。
5.4 权限问题导致 dev 用户读不到
恢复脚本如果用 root 执行,拷进去的文件属主是 root,而 Trae 是以dev用户跑的,读不到这些文件。脚本末尾加一句chown -R dev:dev "$DEST"就能解决。这个坑在set -e下不会报错,但插件就是加载不出来,比较隐蔽。
5.5 vsix_cache 目录为空
如果备份脚本从来没成功跑过,vsix_cache就是空的,恢复脚本自然拷不出东西。先确认备份脚本的SRC指向的是 Trae 实际安装插件的目录,不同版本的 Trae 这个路径可能是~/.trae-server/extensions或~/.trae/extensions,用find /home/dev -name "extensions" -type d找一下真实位置。
6. 把排查流程固化下来
这套排障跑通之后,建议把 Codex 的核对提示词存成一个片段,下次改 compose 或脚本时直接复用。核心就三件事:确认SRC是容器内路径且等于某个 volume 的子目录、确认DEST和 Trae 实际读取目录一致、确认cp失败时脚本不会静默退出。
通道方面,Codex 走 TaoToken 的 Base URL 是 https://taotoken.net/api ,Key 在控制台 API Keys 页面管理。如果只是偶尔排障,模型对话入口够用;如果要长期让 Codex 帮你审配置、跑 Agent 任务,Coding Plan 的额度更合适。接入文档里有各客户端的完整配置示例,照着填就行。
最后提醒一句:docker compose down之前一定先跑备份脚本,别等容器删了才想起来插件没存。养成「改配置先备份、重建先恢复」的习惯,这套环境就能一直稳定用下去。