1025 lines
39 KiB
Bash
Executable File
1025 lines
39 KiB
Bash
Executable File
#!/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 <<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 ""
|