Files
team/.devcontainer/post-create.sh
T
2026-04-19 23:32:19 +08:00

1025 lines
39 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
set -e
# ============================================================================
# 容器创建后配置脚本
# ============================================================================
CONTAINER_USER="${CONTAINER_USER:-vscode}"
PROJECT_NAME="${PROJECT_NAME:-isos-ts}"
GIT_USER_NAME="${GIT_USER_NAME:-arno}"
GIT_USER_EMAIL="${GIT_USER_EMAIL:-arno_jin@bis.com.cn}"
DOCKER_GID="${DOCKER_GID:-984}"
DOCKER_ENABLED="${DOCKER_ENABLED:-true}"
DOCKER_SOCK="${DOCKER_SOCK:-/var/run/docker.sock}"
WORKSPACE="/workspace"
BIN_PATH="${BIN_PATH:-$WORKSPACE/.devcontainer/.volumes/bin}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 加载 .env 文件(补充 docker-compose 未传递的变量)
if [ -f "$SCRIPT_DIR/.env" ]; then
set -a
# shellcheck source=/dev/null
source "$SCRIPT_DIR/.env"
set +a
fi
API_KEY="${API_KEY:-}"
# 引入公共函数库
# shellcheck source=lib.sh
source "${SCRIPT_DIR}/lib.sh"
print_header "🚀 ISOS 开发环境配置"
echo "容器用户: $(text_cyan "$CONTAINER_USER")"
echo "项目路径: $(text_cyan "$WORKSPACE")"
echo ""
# ============================================================================
# 修复 bind-mount volume 权限
# ============================================================================
# Docker Compose 自动创建不存在的 host 目录时,以 root:root 创建。
# 必须在任何写入操作之前修复权限,否则 vscode 用户无法写入。
VOLUME_MOUNTS=(
"/home/vscode/.config/jj"
"/home/vscode/.ssh"
"/home/vscode/.claude"
)
for vol_dir in "${VOLUME_MOUNTS[@]}"; do
if [ -d "$vol_dir" ] && [ "$(stat -c '%U' "$vol_dir")" != "$CONTAINER_USER" ]; then
sudo chown -R "$CONTAINER_USER:$CONTAINER_USER" "$vol_dir"
echo " $(status_ok) 已修复 $vol_dir 权限 → $CONTAINER_USER"
fi
done
# ============================================================================
# 系统配置
# ============================================================================
echo "配置系统..."
# UID/GID 已在 Dockerfile 构建阶段处理,此处仅验证
CURRENT_UID=$(id -u "$CONTAINER_USER" 2>/dev/null || echo "1000")
CURRENT_GID=$(id -g "$CONTAINER_USER" 2>/dev/null || echo "1000")
echo " $(status_info) UID/GID: $CURRENT_UID:$CURRENT_GID"
# 配置时区
echo "设置时区为 Asia/Shanghai..."
[ ! -f /etc/localtime ] && sudo ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime || true
echo "Asia/Shanghai" | sudo tee /etc/timezone >/dev/null
# 配置 Git
echo "配置 Git..."
git config --global user.name "${GIT_USER_NAME}"
git config --global user.email "${GIT_USER_EMAIL}"
git config --global core.autocrlf input
git config --global core.fileMode false
git config --global init.defaultBranch trunk
git config --global --add safe.directory "$WORKSPACE"
# 正确显示中文文件名
git config --global core.quotepath false
# 支持 Unicode 预组合
git config --global core.precomposeunicode true
# 设置 GUI 编码
git config --global gui.encoding utf-8
# 设置提交信息的编码
git config --global i18n.commitencoding utf-8
# 设置日志输出的编码
git config --global i18n.logoutputencoding utf-8
echo "git name: $(git config user.name)"
echo "git email: $(git config user.email)"
# 配置 Jujutsu
if command -v jj &>/dev/null; then
jj config set --user user.name "${GIT_USER_NAME}"
jj config set --user user.email "${GIT_USER_EMAIL}"
jj config set --user 'remotes.origin.auto-track-bookmarks' "'*'"
echo " $(status_ok) jj name: ${GIT_USER_NAME}"
echo " $(status_ok) jj email: ${GIT_USER_EMAIL}"
echo " $(status_ok) jj auto-track-bookmarks: * (origin)"
# 在项目目录初始化 jj(基于已有 .git)
if [ -d "$WORKSPACE/.git" ] && [ ! -d "$WORKSPACE/.jj" ]; then
cd "$WORKSPACE"
jj git init
jj bookmark track trunk --remote=origin 2>/dev/null || true
cd - >/dev/null
echo " $(status_ok) jj 已在项目目录初始化(.jj + .git 并存)"
fi
fi
# 配置 SSH 密钥权限(如果存在)
if [ -f ~/.ssh/id_rsa ]; then
sudo chmod 600 ~/.ssh/id_rsa
fi
if [ -d ~/.ssh ]; then
sudo chmod 700 ~/.ssh
fi
# 配置 Claude Code
echo "配置 Claude Code..."
# 从模板创建 ~/.claude.jsonMCP 服务器配置 + API 密钥)
if [ ! -f ~/.claude.json ]; then
if [ -f "$WORKSPACE/.devcontainer/.volumes/.claude.json" ]; then
cp "$WORKSPACE/.devcontainer/.volumes/.claude.json" ~/.claude.json
echo " $(status_ok) 已从 .volumes 恢复 .claude.json"
elif [ -f "$SCRIPT_DIR/templates/.claude.json" ]; then
if [ -n "$API_KEY" ]; then
sed "s/{{your_api_key}}/$API_KEY/g" "$SCRIPT_DIR/templates/.claude.json" > ~/.claude.json
echo " $(status_ok) 已从模板创建 .claude.jsonAPI 密钥已注入)"
else
cp "$SCRIPT_DIR/templates/.claude.json" ~/.claude.json
echo " $(status_warn) API_KEY 未设置,.claude.json 中的占位符未替换"
fi
else
echo " $(status_warn) 模板文件不存在: templates/.claude.json"
fi
else
echo " $(status_info) .claude.json 已存在,跳过"
fi
# 从模板创建 ~/.claude/settings.jsonANTHROPIC_AUTH_TOKEN + 插件市场)
if [ ! -f ~/.claude/settings.json ]; then
if [ -f "$WORKSPACE/.devcontainer/.volumes/claude/settings.json" ]; then
mkdir -p ~/.claude
cp "$WORKSPACE/.devcontainer/.volumes/claude/settings.json" ~/.claude/settings.json
echo " $(status_ok) 已从 .volumes 恢复 settings.json"
elif [ -f "$SCRIPT_DIR/templates/settings.json" ]; then
mkdir -p ~/.claude
if [ -n "$API_KEY" ]; then
sed "s/{{your_api_key}}/$API_KEY/g" "$SCRIPT_DIR/templates/settings.json" > ~/.claude/settings.json
echo " $(status_ok) 已从模板创建 settings.jsonAPI 密钥已注入)"
else
cp "$SCRIPT_DIR/templates/settings.json" ~/.claude/settings.json
echo " $(status_warn) API_KEY 未设置,settings.json 中的占位符未替换"
fi
else
echo " $(status_warn) 模板文件不存在: templates/settings.json"
fi
else
echo " $(status_info) ~/.claude/settings.json 已存在,跳过"
fi
# 从 tgz 提取 zai-coding-plugins 到固定缓存目录(不依赖 npm/NVM)
PLUGINS_CACHE_DIR="$WORKSPACE/.devcontainer/.cache/claude-plugins"
ZAI_PLUGINS_DST="$PLUGINS_CACHE_DIR/zai-coding-plugins"
ZAI_TGZ=$(ls "$WORKSPACE/.devcontainer/.cache/npm/z_ai-coding-helper-"*.tgz 2>/dev/null | head -n1)
if [ -n "$ZAI_TGZ" ] && [ -f "$ZAI_TGZ" ]; then
# 比较版本标记文件决定是否需要重新提取
TGZ_VERSION=$(basename "$ZAI_TGZ" | sed 's/z_ai-coding-helper-//' | sed 's/\.tgz$//')
CACHE_VERSION=""
[ -f "$ZAI_PLUGINS_DST/.version" ] && CACHE_VERSION=$(cat "$ZAI_PLUGINS_DST/.version" 2>/dev/null)
if [ "$TGZ_VERSION" != "$CACHE_VERSION" ]; then
mkdir -p "$PLUGINS_CACHE_DIR"
rm -rf "$ZAI_PLUGINS_DST"
if tar -xzf "$ZAI_TGZ" -C "$PLUGINS_CACHE_DIR" --strip-components=1 "package/zai-coding-plugins" 2>/dev/null; then
echo "$TGZ_VERSION" > "$ZAI_PLUGINS_DST/.version"
echo " $(status_ok) zai-coding-plugins $(text_green "$TGZ_VERSION") 已提取到缓存"
else
echo " $(status_warn) zai-coding-plugins 提取失败"
fi
else
echo " $(status_info) zai-coding-plugins 缓存已是最新 $(text_green "v$CACHE_VERSION")"
fi
elif [ -d "$ZAI_PLUGINS_DST" ]; then
echo " $(status_info) zai-coding-plugins 缓存已存在(无 tgz 源文件)"
else
echo " $(status_warn) 未找到 @z_ai/coding-helper tgz 文件,zai-coding-plugins 不可用"
fi
if [ -f "$SCRIPT_DIR/templates/CLAUDE.md" ]; then
mkdir -p ~/.claude
if [ ! -f ~/.claude/CLAUDE.md ]; then
cp "$SCRIPT_DIR/templates/CLAUDE.md" ~/.claude/CLAUDE.md
echo " $(status_ok) 已复制 CLAUDE.md 到 ~/.claude/"
else
echo " $(status_info) CLAUDE.md 已存在,跳过复制"
fi
fi
# ============================================================================
# VS Code 全局设置(禁止扩展自动更新,锁定 Claude Code 版本)
# ============================================================================
echo "配置 VS Code 全局设置..."
VSCODE_MACHINE_DIR="$HOME/.vscode-server/data/Machine"
mkdir -p "$VSCODE_MACHINE_DIR"
VSCODE_SETTINGS_FILE="$VSCODE_MACHINE_DIR/settings.json"
if [ ! -f "$VSCODE_SETTINGS_FILE" ]; then
cat > "$VSCODE_SETTINGS_FILE" << 'SETTINGSJSON'
{
"extensions.autoUpdate": "false",
"extensions.autoCheckUpdates": false
}
SETTINGSJSON
echo " $(status_ok) 已创建 VS Code 设置(禁止扩展自动更新)"
else
# 已有设置文件,合并缺失的配置项
UPDATED=false
if ! grep -q '"extensions.autoUpdate"' "$VSCODE_SETTINGS_FILE" 2>/dev/null; then
UPDATED=true
fi
if ! grep -q '"extensions.autoCheckUpdates"' "$VSCODE_SETTINGS_FILE" 2>/dev/null; then
UPDATED=true
fi
if [ "$UPDATED" = true ]; then
# 使用 Python 合并 JSON(容器内已有 python3
python3 -c "
import json, sys
with open('$VSCODE_SETTINGS_FILE') as f:
s = json.load(f)
s['extensions.autoUpdate'] = 'false'
s['extensions.autoCheckUpdates'] = False
with open('$VSCODE_SETTINGS_FILE', 'w') as f:
json.dump(s, f, indent=2, ensure_ascii=False)
" 2>/dev/null && echo " $(status_ok) 已更新 VS Code 设置(禁止扩展自动更新)" || \
echo " $(status_warn) VS Code 设置合并失败"
else
echo " $(status_info) VS Code 设置已包含扩展更新控制"
fi
fi
# 检查并创建 runcc.sh(从模板复制)
RUNCC_SCRIPT="$WORKSPACE/.devcontainer/.volumes/bin/runcc.sh"
RUNCC_TEMPLATE="$SCRIPT_DIR/templates/runcc.sh"
if [ ! -f "$RUNCC_SCRIPT" ]; then
mkdir -p "$(dirname "$RUNCC_SCRIPT")"
cp "$RUNCC_TEMPLATE" "$RUNCC_SCRIPT"
chmod +x "$RUNCC_SCRIPT"
echo " $(status_ok) 已从模板创建 runcc.sh"
fi
# 配置 Open Code
echo "配置 Open Code..."
if [ -f "$WORKSPACE/.devcontainer/opencode.json.example" ]; then
mkdir -p ~/.config/opencode
if [ ! -f ~/.config/opencode/opencode.json ]; then
# 优先使用 .volumes 目录中的配置文件,这个是实际使用中的文件备份的
if [ -f "$WORKSPACE/.devcontainer/.volumes/opencode.json" ]; then
cp "$WORKSPACE/.devcontainer/.volumes/opencode.json" ~/.config/opencode/opencode.json
else
cp "$WORKSPACE/.devcontainer/opencode.json.example" ~/.config/opencode/opencode.json
fi
echo " $(status_ok) 已复制 opencode.json"
else
echo " $(status_info) opencode.json.json 已存在,跳过复制"
fi
fi
# ============================================================================
# Shell 环境配置
# ============================================================================
# 注意:基础环境变量(UV_INDEX_URL、UV_LINK_MODE、PYTHONPATH、PATH 等)
# 已通过 Dockerfile 模板 templates/bashrc.tail.sh 配置,此处仅处理运行时动态配置
echo "配置 shell 环境(运行时动态配置)..."
# cc/cc4/cc5 别名、NVM 镜像变量已由 Dockerfile 模板 bashrc.tail.sh 配置
# ============================================================================
# 镜像源配置
# ============================================================================
echo "配置镜像源..."
append_config ~/.npmrc "registry.npmmirror.com" 'registry="https://registry.npmmirror.com/"'
mkdir -p ~/.pip
if [ ! -f ~/.pip/pip.conf ]; then
cat > ~/.pip/pip.conf <<EOF
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
trusted-host = pypi.tuna.tsinghua.edu.cn/simple
EOF
fi
# ============================================================================
# Xvfb 虚拟显示服务器(支持 Playwright headed 模式)
# ============================================================================
XVFB_DISPLAY=":99"
XVFB_SCREEN="1280x720x24"
XVFB_LOCK="/tmp/.X99-lock"
DISPLAY_ON_HOST="${DISPLAY_ON_HOST:-false}"
print_section "🖥️ 配置显示服务器"
if [ "$DISPLAY_ON_HOST" = "true" ]; then
# 宿主机 X11 转发模式:检测挂载的 X11 socket
HOST_X11_DIR="/tmp/.host-x11"
HOST_DISPLAY_VAL="${HOST_DISPLAY:-:0}"
if [ -d "$HOST_X11_DIR" ] && [ -n "$(ls -A "$HOST_X11_DIR" 2>/dev/null)" ]; then
# 将宿主机 X11 socket 链接到标准位置
mkdir -p /tmp/.X11-unix
ln -sf "$HOST_X11_DIR"/* /tmp/.X11-unix/ 2>/dev/null || true
echo " $(status_ok) X11 宿主机转发模式 (DISPLAY=$(text_cyan "$HOST_DISPLAY_VAL"))"
export DISPLAY="$HOST_DISPLAY_VAL"
append_config ~/.bashrc "DISPLAY" "export DISPLAY=$HOST_DISPLAY_VAL"
else
echo " $(status_warn) DISPLAY_ON_HOST=true 但未检测到 X11 socket,回退到 Xvfb"
DISPLAY_ON_HOST=false
fi
fi
if [ "$DISPLAY_ON_HOST" != "true" ]; then
# 默认模式:启动 Xvfb 虚拟显示
if command -v Xvfb &>/dev/null; then
# 修复 /tmp/.X11-unix 目录权限(Xvfb 要求 root 所有)
if [ -d /tmp/.X11-unix ]; then
sudo chown root:root /tmp/.X11-unix 2>/dev/null || true
sudo chmod 1777 /tmp/.X11-unix 2>/dev/null || true
else
sudo mkdir -p /tmp/.X11-unix 2>/dev/null || true
sudo chmod 1777 /tmp/.X11-unix 2>/dev/null || true
fi
# 启动 Xvfb(如果未运行)
if [ ! -f "$XVFB_LOCK" ] || ! kill -0 "$(cat "$XVFB_LOCK" 2>/dev/null)" 2>/dev/null; then
rm -f "$XVFB_LOCK"
Xvfb "$XVFB_DISPLAY" -screen 0 "$XVFB_SCREEN" -ac +extension GLX +render -noreset 2>/dev/null &
XVFB_PID=$!
sleep 1
if kill -0 "$XVFB_PID" 2>/dev/null; then
echo " $(status_ok) Xvfb 已启动 (PID: $(text_cyan "$XVFB_PID"), DISPLAY=$(text_cyan "$XVFB_DISPLAY"), 分辨率: $(text_cyan "${XVFB_SCREEN%x24}"))"
else
echo " $(status_fail) Xvfb 启动失败"
fi
else
XVFB_PID=$(cat "$XVFB_LOCK" 2>/dev/null)
echo " $(status_info) Xvfb 已在运行 (PID: $(text_cyan "$XVFB_PID"))"
fi
# 设置 DISPLAY 环境变量
export DISPLAY="$XVFB_DISPLAY"
append_config ~/.bashrc "DISPLAY" "export DISPLAY=$XVFB_DISPLAY"
else
echo " $(status_warn) Xvfb 未安装,Playwright 将使用 headless 模式"
fi
fi
# ============================================================================
# Docker 权限配置
# ============================================================================
if [ "$DOCKER_ENABLED" = "true" ]; then
echo "配置 Docker 访问..."
# Docker 组和用户已在 Dockerfile 构建阶段配置
# 此处仅处理运行时 socket GID 不匹配的情况
if [ -S /var/run/docker.sock ]; then
SOCK_GID=$(stat -c '%g' /var/run/docker.sock 2>/dev/null || stat -f '%g' /var/run/docker.sock 2>/dev/null)
if [ -n "$SOCK_GID" ]; then
local_docker_gid=$(getent group docker 2>/dev/null | cut -d: -f3)
if [ -n "$SOCK_GID" ] && [ "$SOCK_GID" != "${local_docker_gid}" ]; then
echo " $(status_info) Docker socket GID ($SOCK_GID) 与 docker 组 GID (${local_docker_gid}) 不同"
SOCK_GROUP=$(getent group "$SOCK_GID" | cut -d: -f1)
if [ -n "$SOCK_GROUP" ] && ! groups "$CONTAINER_USER" | grep -q "$SOCK_GROUP"; then
sudo usermod -aG "$SOCK_GROUP" "$CONTAINER_USER"
echo " $(status_ok) 已将用户添加到 $(text_cyan "$SOCK_GROUP") 组"
fi
fi
fi
fi
else
echo " $(status_info) Docker 已禁用 (DOCKER_ENABLED=$DOCKER_ENABLED)"
fi
# ============================================================================
# 验证 uv 和 Python(已在容器构建阶段安装)
# ============================================================================
echo "验证 uv 和 Python 环境..."
# specify/python3 链接已在 Dockerfile 构建阶段创建
# 检查 uv
if [ -f "$HOME/.local/bin/uv" ]; then
UV_VERSION=$("$HOME/.local/bin/uv" --version 2>/dev/null)
echo " $(status_ok) uv: ${UV_VERSION}"
else
echo " $(status_miss) uv"
fi
# 检查 Python
if [ -f "$HOME/.local/bin/python3" ]; then
PYTHON_VERSION=$("$HOME/.local/bin/python3" --version 2>/dev/null)
echo " $(status_ok) Python: ${PYTHON_VERSION}"
elif [ -f "$HOME/.local/bin/python" ]; then
PYTHON_VERSION=$("$HOME/.local/bin/python" --version 2>/dev/null)
echo " $(status_ok) Python: ${PYTHON_VERSION}"
else
echo " $(status_miss) Python"
fi
# 检查 specify(链接已在构建阶段创建)
if [ -f "$HOME/.local/bin/specify" ]; then
SPEC_KIT_VERSION=$("$HOME/.local/bin/specify" version 2>/dev/null | grep -oP 'CLI Version\s*\K[\d.]+' || echo "已安装")
echo " $(status_ok) specify: ${SPEC_KIT_VERSION}"
elif [ -f "$HOME/.local/share/uv/tools/specify-cli/bin/specify" ]; then
SPEC_KIT_VERSION=$("$HOME/.local/share/uv/tools/specify-cli/bin/specify" version 2>/dev/null | grep -oP 'CLI Version\s*\K[\d.]+' || echo "已安装")
echo " $(status_ok) specify: ${SPEC_KIT_VERSION}"
else
echo " $(status_warn) specify"
fi
# ============================================================================
# 配置 uv Python 缓存目录
# ============================================================================
echo "配置 uv Python 缓存..."
UV_CACHE_DIR="$WORKSPACE/.devcontainer/.volumes/uv-cache"
mkdir -p "$UV_CACHE_DIR"
export UV_CACHE_DIR="$UV_CACHE_DIR"
echo " $(status_ok) uv 缓存目录: $(text_cyan "$UV_CACHE_DIR")"
# ============================================================================
# 配置 electron-builder 缓存目录
# ============================================================================
echo "配置 electron-builder 缓存..."
ELECTRON_BUILDER_CACHE="$WORKSPACE/.devcontainer/.cache/electron-builder"
mkdir -p "$ELECTRON_BUILDER_CACHE"
export ELECTRON_BUILDER_CACHE="$ELECTRON_BUILDER_CACHE"
echo " $(status_ok) electron-builder 缓存目录: $(text_cyan "$ELECTRON_BUILDER_CACHE")"
# 创建符号链接,让 electron-builder 使用项目内缓存
if [ ! -L ~/.cache/electron-builder ]; then
rm -rf ~/.cache/electron-builder 2>/dev/null || true
ln -sf "$ELECTRON_BUILDER_CACHE" ~/.cache/electron-builder
echo " $(status_ok) 已创建符号链接: ~/.cache/electron-builder -> $ELECTRON_BUILDER_CACHE"
else
echo " $(status_info) 符号链接已存在"
fi
if [ -d "$BIN_PATH" ] && [ -n "$(ls -A "$BIN_PATH" 2>/dev/null)" ]; then
TOOL_COUNT=$(ls -1 "$BIN_PATH" 2>/dev/null | wc -l)
echo " $(status_info) .volumes/bin 可用工具: $(text_yellow "$TOOL_COUNT") 个"
fi
echo ""
echo "$(text_bold '✨ 开发环境设置完成!')"
echo ""
echo "$(text_yellow '可用工具:')"
echo " • Python 3.12"
echo " • uv (Python 包管理器)"
echo " • Node.js (由 nvm 管理)"
echo " • Git"
echo " • Jujutsu (jj)"
if [ "$DOCKER_ENABLED" = "true" ]; then
echo " • Docker"
fi
echo " • Spec-Kit Specify CLI"
echo ""
# ============================================================================
# 加载 NVM 环境
# ============================================================================
NVM_LOADED=false
if [ -f "$HOME/.nvm/nvm.sh" ]; then
export NVM_DIR="$HOME/.nvm"
# shellcheck source=/dev/null
set +u # 临时禁用 -u 选项,因为 nvm.sh 可能使用未设置变量
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
set -u # 恢复 -u 选项
# 验证 nvm 命令是否可用
if command -v nvm &>/dev/null || type nvm &>/dev/null; then
NVM_LOADED=true
# 将 NVM Node.js 路径添加到 PATH(如果存在)
if [ -d "$NVM_DIR/versions/node" ]; then
NODE_PATH=$(find "$NVM_DIR/versions/node" -maxdepth 1 -type d -name "v*" | head -n1)
if [ -n "$NODE_PATH" ] && [ -d "$NODE_PATH/bin" ]; then
export PATH="$NODE_PATH/bin:$PATH"
fi
fi
fi
fi
nvm use v22.22.0
# ============================================================================
# 修复 opencode-ai 平台包
# ============================================================================
print_section "🔧 修复 opencode-ai 平台包"
if [ "$NVM_LOADED" = true ] && command -v npm &>/dev/null; then
# 检查 opencode-ai 主包是否已安装
if npm list -g --depth=0 2>/dev/null | grep -q "opencode-ai@"; then
OPCODE_VERSION=$(npm list -g --depth=0 opencode-ai 2>/dev/null | grep opencode-ai | sed 's/.*@//' || echo "")
echo " $(status_info) opencode-ai 主包已安装: v${OPCODE_VERSION}"
# 检查平台包是否需要安装
if ! npm list -g --depth=0 2>/dev/null | grep -q "opencode-linux-x64@"; then
echo " $(status_warn) opencode-linux-x64 平台包未安装"
# 尝试从缓存目录安装平台包
PLATFORM_TARBALL="$WORKSPACE/.devcontainer/.cache/npm/opencode-linux-x64-${OPCODE_VERSION}.tgz"
if [ -f "$PLATFORM_TARBALL" ]; then
echo " $(status_info) 从缓存安装平台包: $(text_cyan "$PLATFORM_TARBALL")"
if npm install -g "$PLATFORM_TARBALL" 2>/dev/null; then
echo " $(status_ok) opencode-linux-x64 安装成功"
else
echo " $(status_fail) opencode-linux-x64 安装失败"
fi
else
echo " $(status_warn) 平台包缓存文件不存在: $PLATFORM_TARBALL"
echo " $(status_info) 尝试从 npm registry 安装..."
if npm install -g "opencode-linux-x64@${OPCODE_VERSION}" --registry https://registry.npmjs.org/ 2>/dev/null; then
echo " $(status_ok) opencode-linux-x64 安装成功"
else
echo " $(status_fail) opencode-linux-x64 安装失败"
fi
fi
else
PLATFORM_VERSION=$(npm list -g --depth=0 opencode-linux-x64 2>/dev/null | grep opencode-linux-x64 | sed 's/.*@//' || echo "")
echo " $(status_ok) opencode-linux-x64 平台包已安装: v${PLATFORM_VERSION}"
fi
else
echo " $(status_info) opencode-ai 未安装,跳过平台包检查"
fi
else
echo " $(status_warn) npm 环境未就绪,跳过 opencode-ai 检查"
fi
echo ""
# ============================================================================
# 修复 Claude Code ripgrep 执行权限
# ============================================================================
print_section "🔧 修复 Claude Code ripgrep 执行权限"
if [ "$NVM_LOADED" = true ] && [ -d "$NVM_DIR/versions/node" ]; then
NODE_PATH=$(find "$NVM_DIR/versions/node" -maxdepth 1 -type d -name "v*" | head -n1)
RG_PATH=$(find "$NODE_PATH/lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep" -name "rg" 2>/dev/null | head -n1)
if [ -n "$RG_PATH" ]; then
if [ ! -x "$RG_PATH" ]; then
chmod +x "$RG_PATH"
echo " $(status_ok) 已修复 rg 执行权限: $(text_cyan "$RG_PATH")"
else
echo " $(status_ok) rg 执行权限正常"
fi
else
echo " $(status_info) 未找到 Claude Code ripgrep,跳过"
fi
else
echo " $(status_info) NVM 未加载,跳过 ripgrep 权限检查"
fi
echo ""
# ============================================================================
# 配置 Coding Helper API 密钥
# ============================================================================
print_section "🔑 配置 Coding Helper API 密钥"
if [ "$NVM_LOADED" = true ] && command -v npm &>/dev/null; then
HELPER_GLOBAL_PATH="$(npm root -g 2>/dev/null)/@z_ai/coding-helper"
if [ -d "$HELPER_GLOBAL_PATH" ]; then
HELPER_VERSION=$(npm list -g --depth=0 @z_ai/coding-helper 2>/dev/null | grep @z_ai/coding-helper | sed 's/.*@//' || echo "unknown")
echo " $(status_ok) @z_ai/coding-helper: $(text_green "$HELPER_VERSION")"
# 使用 coding-helper CLI 注册 API 密钥
if [ -n "$API_KEY" ]; then
echo " $(status_info) 注册 API 密钥..."
if coding-helper auth glm_coding_plan_china "$API_KEY" 2>/dev/null; then
echo " $(status_ok) API 密钥已注册(glm_coding_plan_china"
else
echo " $(status_warn) API 密钥注册失败"
fi
if coding-helper auth reload claude 2>/dev/null; then
echo " $(status_ok) Claude Code 配置已重载"
else
echo " $(status_warn) Claude Code 配置重载失败"
fi
# 尝试为 Open Code 也重载配置
if coding-helper auth reload opencode 2>/dev/null; then
echo " $(status_ok) Open Code 配置已重载"
fi
else
echo " $(status_warn) API_KEY 未设置,跳过密钥注册"
echo " $(status_info) 请在 .env 文件中设置 API_KEY"
fi
else
echo " $(status_warn) @z_ai/coding-helper 未全局安装,跳过"
fi
else
echo " $(status_warn) npm 环境未就绪,跳过 Coding Helper 配置"
fi
echo ""
# ============================================================================
# Claude Code MCP 服务器和插件配置
# ============================================================================
print_section "🔌 配置 Claude Code MCP 服务器和插件"
if command -v claude &>/dev/null; then
# --- 注册 MCP 服务器 ---
echo "注册 MCP 服务器..."
# Chrome DevTools MCP(连接 Chrome 开发者工具,用于调试网页)
if ! claude mcp list 2>/dev/null | grep -q "chrome-devtools"; then
claude mcp add --transport stdio chrome-devtools -- npx -y chrome-devtools-mcp 2>/dev/null && \
echo " $(status_ok) chrome-devtools MCP 已注册" || \
echo " $(status_warn) chrome-devtools MCP 注册失败"
else
echo " $(status_info) chrome-devtools MCP 已存在"
fi
# Playwright MCP 由 playwright@claude-plugins-official 插件提供
# --- 注册插件市场 ---
echo ""
echo "注册插件市场..."
PLUGIN_CACHE_DIR="$WORKSPACE/.devcontainer/.cache/claude-plugins"
PLUGIN_MARKETPLACE_DIR="$HOME/.claude/plugins/marketplaces"
# claude-plugins-official: 通过目录覆盖方式使用本地缓存
OFFICIAL_CACHE="$PLUGIN_CACHE_DIR/claude-plugins-official"
OFFICIAL_TARGET="$PLUGIN_MARKETPLACE_DIR/claude-plugins-official"
if [ -d "$OFFICIAL_CACHE" ]; then
mkdir -p "$PLUGIN_MARKETPLACE_DIR"
rm -rf "$OFFICIAL_TARGET"
cp -r "$OFFICIAL_CACHE" "$OFFICIAL_TARGET"
echo " $(status_ok) claude-plugins-official: 从本地缓存覆盖 $(text_cyan "$OFFICIAL_CACHE")"
else
echo " $(status_info) claude-plugins-official: 本地缓存不存在,使用默认"
fi
# 通过 CLI 注册的市场(优先使用本地缓存,无需网络)
# 格式: "本地缓存目录名:远程 URL"
PLUGIN_REGISTRIES=(
"ui-ux-pro-max-skill:https://gitea.szis.com.cn/github/nextlevelbuilder-ui-ux-pro-max-skill"
"svelte:https://gitea.szis.com.cn/github/sveltejs-ai-tools"
"superpowers-marketplace:https://gitea.szis.com.cn/github/obra-superpowers"
"zai-coding-plugins:" # 纯本地市场,无远程 URL
)
for reg_entry in "${PLUGIN_REGISTRIES[@]}"; do
reg_name="${reg_entry%%:*}"
reg_url="${reg_entry#*:}"
# 优先使用本地缓存的仓库
local_cache="$PLUGIN_CACHE_DIR/$reg_name"
if [ -d "$local_cache" ]; then
echo " $(status_info) ${reg_name}: 使用本地缓存 $(text_cyan "$local_cache")"
reg_url="$local_cache"
fi
# 跳过无缓存且无 URL 的市场
if [ -z "$reg_url" ]; then
echo " $(status_warn) ${reg_name}: 无本地缓存且无远程 URL,跳过"
continue
fi
# 检查是否已注册(匹配市场名,不依赖 CLI 输出中的 ❯ 前缀)
if claude plugin marketplace list 2>/dev/null | grep -q "${reg_name}"; then
echo " $(status_info) 插件市场 ${reg_name} 已注册"
else
if claude plugin marketplace add "$reg_url" --scope project; then
echo " $(status_ok) 插件市场 ${reg_name} 已注册"
else
echo " $(status_warn) 插件市场 ${reg_name} 注册失败"
fi
fi
done
# --- 安装插件 ---
echo ""
echo "安装 Claude Code 插件..."
# 格式: "插件名@市场名"
INSTALL_PLUGINS=(
"code-simplifier@claude-plugins-official"
"frontend-design@claude-plugins-official"
"superpowers@claude-plugins-official"
"code-review@claude-plugins-official"
"hookify@claude-plugins-official"
"ralph-loop@claude-plugins-official"
"skill-creator@claude-plugins-official"
"typescript-lsp@claude-plugins-official"
"pyright-lsp@claude-plugins-official"
"playwright@claude-plugins-official"
"ui-ux-pro-max@ui-ux-pro-max-skill"
"svelte@svelte"
# Superpowers Marketplace 插件(市场 internal name 是 superpowers-dev
"superpowers@superpowers-dev"
# Z AI Coding Plan 插件
"glm-plan-usage@zai-coding-plugins"
)
# 只调用一次 claude plugin list,缓存结果避免重复启动 CLI 进程
INSTALLED_PLUGINS=$(claude plugin list 2>/dev/null || true)
for plugin_entry in "${INSTALL_PLUGINS[@]}"; do
plugin_name="${plugin_entry%%@*}"
if echo "$INSTALLED_PLUGINS" | grep -q "${plugin_name}@"; then
echo " $(status_info) ${plugin_name} 已安装"
else
# 串行安装:避免并发写入项目配置文件导致竞态冲突
if claude plugin install "${plugin_entry}" --scope project 2>/dev/null; then
echo " $(status_ok) ${plugin_name} 安装成功"
else
echo " $(status_warn) ${plugin_name} 安装失败"
fi
fi
done
else
echo " $(status_warn) claude 命令不可用,跳过 MCP 和插件配置"
fi
echo ""
# ============================================================================
# 环境验证
# ============================================================================
print_section "🔍 测试工具可用性"
# Node.js 生态
echo ""
echo "$(text_cyan '📦 Node.js 生态:')$(text_reset)"
if [ "$NVM_LOADED" = true ]; then
echo " $(status_ok) NVM"
else
echo " $(status_miss) NVM"
fi
check_tool "Node.js" "node" "node --version"
# Node.js 全局包
echo ""
echo "$(text_blue '📦 Node.js 全局包:')"
if [ "$NVM_LOADED" = true ] && command -v npm &>/dev/null; then
npm_packages=(
"npm:npm"
"typescript:TypeScript"
"typescript-language-server:TypeScript Language Server"
"tsx:tsx"
"@anthropic-ai/claude-code:Claude Code"
"chrome-devtools-mcp:chrome-devtools-mcp"
"@playwright/mcp:Playwright MCP"
"pyright:pyright"
"svelte-language-server:Svelte Language Server"
"uipro-cli:UIPro CLI"
"bun:bun"
"opencode-ai:OpenCode AI"
"opencode-linux-x64:OpenCode Linux Platform"
"@z_ai/coding-helper:Z AI Coding Helper"
"tsgo-dev:tsgo-dev"
)
for pkg_entry in "${npm_packages[@]}"; do
pkg_name="${pkg_entry%%:*}"
display_name="${pkg_entry#*:}"
if npm list -g --depth=0 "$pkg_name" 2>/dev/null | grep -q "$pkg_name"; then
version=$(npm list -g --depth=0 "$pkg_name" 2>/dev/null | grep "$pkg_name" | sed 's/.*@//')
if [ -n "$version" ]; then
echo " $(status_ok) ${display_name}: $(text_green "$version")"
else
echo " $(status_ok) ${display_name}"
fi
else
echo " $(status_miss) ${display_name}"
fi
done
else
echo " $(status_warn) npm 环境未就绪,跳过包检查"
fi
# TypeScript Go 生态
echo ""
echo "$(text_cyan '🔷 TypeScript Go 生态:')"
check_tool "tsgo" "tsgo" "tsgo --version"
# Python 生态
echo ""
echo "$(text_green '🐍 Python 生态:')"
# 检查 Python 3(优先检查 .local/bin
if [ -f "$HOME/.local/bin/python3" ]; then
PYTHON_VERSION=$("$HOME/.local/bin/python3" --version 2>/dev/null)
echo " $(status_ok) Python 3: ${PYTHON_VERSION}"
elif command -v python3 &>/dev/null; then
PYTHON_VERSION=$(python3 --version 2>/dev/null)
echo " $(status_ok) Python 3: ${PYTHON_VERSION}"
else
echo " $(status_miss) Python 3"
fi
if [ -f "$HOME/.local/bin/uv" ]; then
UV_VERSION=$("$HOME/.local/bin/uv" --version 2>/dev/null)
echo " $(status_ok) uv: ${UV_VERSION}"
else
echo " $(status_warn) uv"
fi
# 检查 specify
if [ -f "$HOME/.local/bin/specify" ]; then
SPEC_KIT_VERSION=$("$HOME/.local/bin/specify" version 2>/dev/null | grep -oP 'CLI Version\s*\K[\d.]+' || echo "已安装")
echo " $(status_ok) specify: ${SPEC_KIT_VERSION}"
elif [ -f "$HOME/.local/share/uv/tools/specify-cli/bin/specify" ]; then
SPEC_KIT_VERSION=$("$HOME/.local/share/uv/tools/specify-cli/bin/specify" version 2>/dev/null | grep -oP 'CLI Version\s*\K[\d.]+' || echo "已安装")
echo " $(status_ok) specify: ${SPEC_KIT_VERSION}"
else
echo " $(status_warn) specify"
fi
# .volumes/bin 工具列表
if [ -d "$BIN_PATH" ]; then
TOOL_COUNT=$(ls -1 "$BIN_PATH" 2>/dev/null | wc -l)
if [ "$TOOL_COUNT" -gt 0 ]; then
echo " $(status_info) .volumes/bin 中的工具 ($(text_yellow "$TOOL_COUNT") 个):"
ls -1 "$BIN_PATH" 2>/dev/null | head -10 | while read -r tool; do
echo " - $(text_cyan "$tool")"
done
if [ "$TOOL_COUNT" -gt 10 ]; then
echo " ... 还有 $(text_yellow "$((TOOL_COUNT - 10))") 个工具"
fi
fi
fi
# Docker
echo ""
echo "$(text_cyan '🐳 Docker:')"
if [ "$DOCKER_ENABLED" = "true" ]; then
check_tool "Docker CLI" "docker" "docker --version"
if command -v docker &>/dev/null; then
if docker ps &>/dev/null; then
echo " $(status_ok) Docker 守护进程连接"
else
echo " $(status_warn) Docker 守护进程连接失败"
fi
fi
if groups | grep -q docker; then
echo " $(status_ok) Docker 组权限"
else
echo " $(status_warn) Docker 组权限"
fi
else
echo " $(status_info) Docker 已禁用 (DOCKER_ENABLED=$DOCKER_ENABLED)"
fi
# 系统工具
echo ""
echo "$(text_yellow '🔧 系统工具:')"
# Playwright 浏览器验证(使用系统 Chrome,通过 --browser chrome 复用)
if command -v google-chrome &>/dev/null; then
CHROME_VERSION=$(google-chrome --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+\.\d+' || echo "unknown")
echo " $(status_ok) Playwright MCP: $(text_green "使用系统 Chrome ${CHROME_VERSION}")"
else
echo " $(status_warn) 系统 Chrome 未安装,Playwright MCP 将无法工作"
fi
check_tool "Google Chrome" "google-chrome" "google-chrome --version"
check_tool "Git" "git" "git --version"
check_tool "Jujutsu (jj)" "jj" "jj --version"
check_tool "Vim" "vim" "vim --version | head -n1"
check_tool "curl" "curl" "curl --version | head -n1"
# VSCode 扩展
echo ""
echo "$(text_blue '🧩 VSCode 扩展:')"
if [ -d "$HOME/.vscode-server/extensions" ]; then
ext_count=$(find "$HOME/.vscode-server/extensions" -maxdepth 1 -type d -name "*-*" 2>/dev/null | wc -l)
echo " $(status_ok) 已安装 $(text_green "$ext_count") 个扩展"
else
echo " $(status_warn) VSCode Server 扩展目录不存在"
fi
# 环境配置
echo ""
echo "$(text_purple '⚙️ 环境配置:')"
CURRENT_TZ=$(cat /etc/timezone 2>/dev/null || echo "")
if [ "$CURRENT_TZ" = "Asia/Shanghai" ]; then
echo " $(status_ok) 时区: $(text_green "$CURRENT_TZ")"
else
echo " $(status_warn) 时区: $(text_yellow "$CURRENT_TZ") (建议: $(text_cyan "Asia/Shanghai"))"
fi
if [ "$LANG" = "zh_CN.UTF-8" ]; then
echo " $(status_ok) 语言: $(text_green "$LANG")"
else
echo " $(status_warn) 语言: $(text_yellow "$LANG") (建议: $(text_cyan "zh_CN.UTF-8"))"
fi
# UV 缓存目录
if [ -n "$UV_CACHE_DIR" ] && [ -d "$UV_CACHE_DIR" ]; then
cache_size=$(du -sh "$UV_CACHE_DIR" 2>/dev/null | cut -f1)
echo " $(status_ok) UV_CACHE_DIR: $(text_cyan "$UV_CACHE_DIR") ($(text_gray "$cache_size"))"
else
echo " $(status_warn) UV_CACHE_DIR"
fi
# 镜像源配置
echo ""
echo "$(text_cyan '📡 镜像源配置:')"
check_config ~/.npmrc "registry.npmmirror.com" "npm 镜像"
echo " npm: https://registry.npmmirror.com/"
check_config ~/.bashrc "NVM_NODEJS_ORG_MIRROR" "NVM Node.js 镜像"
echo " NVM Node.js: https://npmmirror.com/mirrors/node"
check_config ~/.pip/pip.conf "pypi.tuna.tsinghua.edu.cn/simple" "pip 镜像"
echo " pip: https://pypi.tuna.tsinghua.edu.cn/simple"
if [ -n "$UV_INDEX_URL" ]; then
if echo "$UV_INDEX_URL" | grep -q "pypi.tuna.tsinghua.edu.cn/simple"; then
echo " $(status_ok) UV 镜像配置"
else
echo " $(status_warn) UV 镜像: $(text_yellow "$UV_INDEX_URL")"
fi
echo " UV: https://pypi.tuna.tsinghua.edu.cn/simple"
else
echo " $(status_warn) UV_INDEX_URL 未设置"
fi
# 镜像源可用性测试
echo ""
echo "$(text_blue '🌐 镜像源可用性:')"
test_url "npm 镜像" "https://registry.npmmirror.com" || true
test_url "npm 官方源" "https://registry.npmjs.org" || true
test_url "pip 镜像" "https://pypi.tuna.tsinghua.edu.cn/simple" || true
test_url "Node.js 镜像" "https://cdn.npmmirror.com/binaries/node/index.json" || true
# 网络连接
echo ""
echo "$(text_cyan '🌍 网络连接:')"
if command -v curl &>/dev/null; then
if curl -s --connect-timeout 3 https://www.baidu.com >/dev/null 2>&1; then
echo " $(status_ok) 外部网络访问"
else
echo " $(status_warn) 外部网络访问受限"
if [ -n "$http_proxy" ] || [ -n "$https_proxy" ]; then
echo " $(status_info) 已配置代理: $(text_cyan "$http_proxy")"
fi
fi
else
echo " $(status_warn) curl 不可用"
fi
echo ""
cat > ~/.tmux.conf << 'EOF'
# 启用鼠标支持(可以鼠标点击切换面板/窗口)
set -g mouse on
# 允许直接传递通知
set -g allow-passthrough on
# 设置窗口从1开始编号(默认是0
set -g base-index 1
setw -g pane-base-index 1
# 自定义状态栏样式
set -g status-bg black
set -g status-fg green
# 状态栏 左侧显示会话名、窗口编号、窗口名称、面板编号和当前时间
set -g status-left-length 50
set -g status-left "#S:#I #W"
# 状态栏 中间显示当前窗口的命令
set -g window-status-format "#P #T"
set -g window-status-current-format "#P #T"
set -g status-justify centre
# 状态栏 右侧显示tmux版本和当前时间
set -g status-right "v#{version} %Y-%m-%d %H:%M"
EOF
echo ""
print_section "✅ 开发环境检查完成!"
echo ""