#!/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.json(MCP 服务器配置 + 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.json(API 密钥已注入)" 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.json(ANTHROPIC_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.json(API 密钥已注入)" 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 </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 ""