@@ -0,0 +1,100 @@
|
||||
# ============================================================================
|
||||
# 开发容器环境变量配置
|
||||
# ============================================================================
|
||||
#
|
||||
# 使用说明:
|
||||
# 1. 复制此文件: cp .env.example .env
|
||||
# 2. 根据实际情况修改下面的配置项
|
||||
# 3. 运行启动脚本: bash start.sh
|
||||
#
|
||||
# ============================================================================
|
||||
|
||||
# 容器用户 UID/GID
|
||||
# 用途: 容器内用户的 UID 和 GID
|
||||
# 建议: 设置为与宿主机当前用户一致的 UID/GID,避免文件权限问题
|
||||
# 获取方式: 在宿主机运行 'id -u' 和 'id -g'
|
||||
CONTAINER_USER_UID=1000
|
||||
CONTAINER_USER_GID=1000
|
||||
|
||||
# 项目名称
|
||||
# 用途: 容器名称、主机名等标识
|
||||
PROJECT_NAME=team
|
||||
|
||||
# 持久化卷路径配置
|
||||
# 用途: 指定 SSH、Claude Code、jj 等配置的持久化路径
|
||||
CLAUDE_PATH=.volumes/claude
|
||||
SSH_PATH=.volumes/ssh
|
||||
JJ_CONFIG_PATH=.volumes/jj
|
||||
|
||||
# Docker 配置
|
||||
# 用途: 控制容器内是否启用 Docker 访问
|
||||
# 设置为 false 时,不会配置 Docker 组权限,也不会挂载 Docker socket
|
||||
DOCKER_ENABLED=false
|
||||
|
||||
# Docker Socket 路径
|
||||
# 用途: 指定 Docker socket 文件的挂载源路径
|
||||
# 当 DOCKER_ENABLED=true 时,设置为 /var/run/docker.sock 以启用 Docker socket 挂载
|
||||
# 当 DOCKER_ENABLED=false 时,设置为 /dev/null 以禁用 Docker socket 挂载
|
||||
DOCKER_SOCK=/var/run/docker.sock
|
||||
|
||||
# Docker 组 GID
|
||||
# 用途: 容器内 docker 组的 GID,需与宿主机 docker 组 GID 一致
|
||||
# 获取方式: 在宿主机运行 'getent group docker | cut -d: -f3'
|
||||
# 仅在 DOCKER_ENABLED=true 时生效
|
||||
DOCKER_GID=984
|
||||
|
||||
# X11 宿主机显示配置
|
||||
# 用途: 控制容器内 GUI 应用(如 Playwright headed 模式)是否在宿主机显示
|
||||
# 设置为 true 时,需要宿主机运行 X Server 并允许容器连接
|
||||
# 设置为 false 时(默认),容器内使用 Xvfb 虚拟显示
|
||||
# 前提: 仅 Linux 宿主机支持,macOS/Windows 需使用 VNC 方案
|
||||
DISPLAY_ON_HOST=false
|
||||
|
||||
# 宿主机 X11 Display(通常无需修改,由宿主机自动获取)
|
||||
# 获取方式: 在宿主机运行 'echo $DISPLAY'
|
||||
HOST_DISPLAY=:0
|
||||
|
||||
# Git 用户配置
|
||||
# 用途: 容器内的 Git 全局配置
|
||||
GIT_USER_NAME=arno
|
||||
GIT_USER_EMAIL=arno_jin@bis.com.cn
|
||||
|
||||
# API 密钥配置
|
||||
# 用途: 智谱 GLM Coding Plan API 密钥,用于 Claude Code 和 Open Code 认证
|
||||
# 获取方式: 在 https://open.bigmodel.cn/ 注册并获取 API Key
|
||||
# 注意: 此密钥会注入到 ~/.claude.json(MCP 服务器)和 ~/.claude/settings.json(AUTH_TOKEN)
|
||||
API_KEY=
|
||||
|
||||
# 容器资源限制
|
||||
# 用途: 控制容器可使用的 CPU 核心数和内存上限
|
||||
# 建议: 根据宿主机资源情况调整,开发环境推荐 4 核 8G 起步
|
||||
CONTAINER_CPUS=6
|
||||
CONTAINER_MEMORY=16G
|
||||
|
||||
# ============================================================================
|
||||
# 工具版本配置(download-resources.sh 使用)
|
||||
# ============================================================================
|
||||
|
||||
# UV 包管理器版本
|
||||
UV_VERSION=0.10.4
|
||||
|
||||
# NVM 版本
|
||||
NVM_VERSION=v0.40.3
|
||||
|
||||
# Node.js 版本
|
||||
NODE_VERSION=22.22.0
|
||||
|
||||
# npm 版本
|
||||
NPM_VERSION=11.12.1
|
||||
|
||||
# Google Chrome 版本
|
||||
CHROME_VERSION=146.0.7680.177-1
|
||||
|
||||
# Python 大版本号(用于 uv python-build-standalone)
|
||||
PYTHON_VERSION=3.12
|
||||
|
||||
# Electron 版本
|
||||
ELECTRON_VERSION=v32.3.3
|
||||
|
||||
# jj (Jujutsu) 版本控制工具版本
|
||||
JJ_VERSION=v0.40.0
|
||||
@@ -0,0 +1,229 @@
|
||||
# 使用本地缓存资源的 Dockerfile
|
||||
# 构建前请先运行 ./download-resources.sh 下载所需资源
|
||||
# 注意:默认情况下 .env 文件对 Dockerfile 是无效的
|
||||
|
||||
# ============================================================================
|
||||
# 基础镜像: 全局环境变量和标签
|
||||
# ============================================================================
|
||||
FROM mcr.microsoft.com/devcontainers/base:noble AS base_0_5
|
||||
|
||||
ARG CONTAINER_USER_UID=1000
|
||||
ARG CONTAINER_USER_GID=1000
|
||||
ARG PROJECT_NAME=isos
|
||||
ARG DOCKER_GID=984
|
||||
|
||||
ENV TZ="Asia/Shanghai" \
|
||||
CONTAINER_USER_UID="$CONTAINER_USER_UID" \
|
||||
CONTAINER_USER_GID="$CONTAINER_USER_GID" \
|
||||
PROJECT_NAME="$PROJECT_NAME" \
|
||||
DOCKER_GID="$DOCKER_GID" \
|
||||
PROJECT_ROOT="/workspace" \
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
LABEL maintainer="Arno Jin <arno@arnojin.com>" \
|
||||
base.image="mcr.microsoft.com/devcontainers/base:noble" \
|
||||
ubuntu.version="24.04" \
|
||||
uv.version="installed" \
|
||||
spec-kit.version="installed" \
|
||||
container.name="$PROJECT_NAME" \
|
||||
container.hostname="$PROJECT_NAME"
|
||||
|
||||
# ============================================================================
|
||||
# 阶段 1/4: 系统级安装 + Docker CLI(需要 root 权限)
|
||||
# ============================================================================
|
||||
# 注意: mcr.microsoft.com/devcontainers/base:noble 已预装:
|
||||
# 核心: git, curl, wget, sudo, jq, unzip, zip, gnupg, xz-utils, patch
|
||||
# 编辑: vim-tiny, vim-common, nano
|
||||
# 构建: build-essential (gcc, g++, make), libc6-dev, libssl-dev, zlib1g-dev
|
||||
# 网络: iproute2, net-tools, openssh-client
|
||||
# 系统: locales, tzdata, ca-certificates, htop, strace, lsof, ncdu, tree, rsync
|
||||
# Shell: bash, zsh (+ oh-my-zsh)
|
||||
# 用户: vscode (UID/GID 1000, sudo NOPASSWD, shell=/bin/bash)
|
||||
# Root: shell=/bin/bash
|
||||
# 以下工具/库需要额外安装:
|
||||
|
||||
FROM base_0_5 AS system_and_docker_1_4
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
set -eu && \
|
||||
# 配置清华镜像源
|
||||
sed -i 's|http://archive.ubuntu.com|https://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/ubuntu.sources \
|
||||
&& sed -i 's|http://security.ubuntu.com|https://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list.d/ubuntu.sources \
|
||||
# 移除失效的 Yarn APT 源
|
||||
&& rm -f /etc/apt/sources.list.d/yarn.list \
|
||||
/usr/share/keyrings/yarn-archive-keyring.gpg \
|
||||
/etc/apt/sources.list.d/yarn.list.bak 2>/dev/null || true \
|
||||
# 安装系统包
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
bats tmux ssh ipset iptables \
|
||||
iputils-ping dnsutils telnet fzf vim xvfb xauth x11-utils \
|
||||
fonts-inter fonts-noto-cjk fonts-jetbrains-mono fonts-liberation fontconfig fonts-noto-color-emoji \
|
||||
# Playwright Chromium 核心依赖
|
||||
libnss3 libnspr4 libatk1.0-0t64 libatk-bridge2.0-0t64 libatspi2.0-0t64 \
|
||||
libdrm2 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libxext6 \
|
||||
libgbm1 libxkbcommon0 libpango-1.0-0 libcairo2 libcairo-gobject2 \
|
||||
libasound2t64 libcups2t64 libgtk-3-0t64 libvulkan1 xdg-utils \
|
||||
# Playwright Chromium X11/Wayland 依赖
|
||||
libdbus-1-3 libx11-6 libxcb1 libxcursor1 libxi6 libxrender1 \
|
||||
libx11-xcb1 libxcb-shm0 libglib2.0-0t64 \
|
||||
# Playwright Chromium 渲染/媒体依赖
|
||||
libfontconfig1 libfreetype6 libgdk-pixbuf-2.0-0 libpangocairo-1.0-0 \
|
||||
libavcodec60 libopus0 libvpx9 libwebp7 libxslt1.1 libharfbuzz0b \
|
||||
libjpeg-turbo8 libpng16-16t64 libavif16 liblcms2-2 libevent-2.1-7t64 \
|
||||
# GPU 支持
|
||||
libgl1 libegl1 \
|
||||
# --- Docker CLI(DooD 模式,仅 CLI,不含 daemon,使用清华镜像源)---
|
||||
&& install -m 0755 -d /etc/apt/keyrings \
|
||||
&& curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
|
||||
&& chmod a+r /etc/apt/keyrings/docker.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
||||
| tee /etc/apt/sources.list.d/docker.list > /dev/null \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
docker-ce-cli \
|
||||
docker-buildx-plugin \
|
||||
docker-compose-plugin \
|
||||
# --- 系统配置 ---
|
||||
&& locale-gen zh_CN.UTF-8 \
|
||||
&& update-locale LANG=zh_CN.UTF-8 LC_ALL=zh_CN.UTF-8 \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& fc-cache -fv \
|
||||
&& echo 'fs.inotify.max_user_watches=524288' >> /etc/sysctl.d/10-sysctl.conf \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
/tmp/* \
|
||||
/var/tmp/* \
|
||||
/var/cache/apt/archives/*.deb
|
||||
|
||||
# ============================================================================
|
||||
# 阶段 2/4: 安装 Chrome + 配置用户和目录权限(需要 root 权限)
|
||||
# ============================================================================
|
||||
FROM system_and_docker_1_4 AS chrome_2_4
|
||||
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
--mount=type=bind,source=.devcontainer/.cache/chrome,target=/tmp/chrome-cache \
|
||||
set -eu && \
|
||||
apt-get update \
|
||||
&& dpkg -i /tmp/chrome-cache/google-chrome-stable_*.deb || apt-get install -y -f \
|
||||
&& google-chrome --version \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& if [ "$CONTAINER_USER_UID" != "1000" ] || [ "$CONTAINER_USER_GID" != "1000" ]; then \
|
||||
groupmod -g "$CONTAINER_USER_GID" vscode && \
|
||||
usermod -u "$CONTAINER_USER_UID" -d "/home/vscode" vscode && \
|
||||
chown -R "$CONTAINER_USER_UID:$CONTAINER_USER_GID" /home/vscode; \
|
||||
fi \
|
||||
&& groupmod -g ${DOCKER_GID} docker 2>/dev/null || groupadd -g ${DOCKER_GID} docker 2>/dev/null || true \
|
||||
&& usermod -aG docker vscode \
|
||||
&& mkdir -p $PROJECT_ROOT \
|
||||
&& chown -R vscode:vscode $PROJECT_ROOT
|
||||
|
||||
# ============================================================================
|
||||
# 阶段 3/4: 安装开发工具(普通用户)
|
||||
# ============================================================================
|
||||
FROM chrome_2_4 AS dev_tools_3_4
|
||||
|
||||
ENV BIN_PATH="$PROJECT_ROOT/.devcontainer/.volumes/bin" \
|
||||
NVM_DIR="/home/vscode/.nvm" \
|
||||
NODE_VERSION="22.22.0" \
|
||||
UV_PYTHON_DIR="/home/vscode/.local/share/uv" \
|
||||
UV_BIN="/home/vscode/.local/bin" \
|
||||
PATH="/home/vscode/.local/bin:$PATH" \
|
||||
UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple \
|
||||
UV_LINK_MODE=copy \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
COREPACK_ENABLE_DOWNLOAD_PROMPT=false \
|
||||
LC_ALL=zh_CN.UTF-8 \
|
||||
LC_TIME=zh_CN.UTF-8 \
|
||||
LANG=zh_CN.UTF-8 \
|
||||
DEVCONTAINER=true \
|
||||
EDITOR=vim \
|
||||
VISUAL=vim \
|
||||
SHELL=/bin/bash \
|
||||
DISPLAY=""
|
||||
|
||||
WORKDIR $PROJECT_ROOT
|
||||
USER vscode
|
||||
|
||||
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
|
||||
|
||||
# 使用 --mount=type=bind 挂载缓存资源(避免 COPY 层残留临时文件)
|
||||
RUN --mount=type=bind,source=.devcontainer/.cache/uv,target=/tmp/uv-cache \
|
||||
--mount=type=bind,source=.devcontainer/.cache/node,target=/tmp/node-cache \
|
||||
--mount=type=bind,source=.devcontainer/.cache/npm,target=/tmp/npm-cache \
|
||||
--mount=type=bind,source=.devcontainer/.cache/vscode,target=/tmp/vscode-cache \
|
||||
--mount=type=bind,source=.devcontainer/.cache/nvm,target=/tmp/nvm-cache \
|
||||
--mount=type=bind,source=.devcontainer/.cache/spec-kit,target=/tmp/spec-kit-cache \
|
||||
--mount=type=bind,source=.devcontainer/.cache/jj,target=/tmp/jj-cache \
|
||||
mkdir -p "$UV_BIN" "$UV_PYTHON_DIR" "$NVM_DIR" \
|
||||
&& tar -xzf /tmp/uv-cache/uv-x86_64-unknown-linux-gnu.tar.gz -C "$UV_BIN" --strip-components=1 \
|
||||
&& tar -xzf /tmp/uv-cache/cpython-*-x86_64-unknown-linux-gnu-install_only.tar.gz -C "$UV_PYTHON_DIR" \
|
||||
&& ln -sf "$UV_PYTHON_DIR/python/bin/python3.12" "$UV_BIN/python3" \
|
||||
&& ln -sf "$UV_PYTHON_DIR/python/bin/python3.12" "$UV_BIN/python" \
|
||||
&& "$UV_BIN/uv" tool install --python "$UV_PYTHON_DIR/python/bin/python3.12" /tmp/spec-kit-cache \
|
||||
&& ln -sf "/home/vscode/.local/share/uv/tools/specify-cli/bin/specify" "$UV_BIN/specify" \
|
||||
&& cp -r /tmp/nvm-cache/. "$NVM_DIR/" \
|
||||
&& [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" \
|
||||
&& mkdir -p "$NVM_DIR/versions/node" \
|
||||
&& tar -xJf /tmp/node-cache/node-v*.tar.xz -C "$NVM_DIR/versions/node" \
|
||||
&& NODE_DIR=$(ls -d "$NVM_DIR/versions/node"/node-v* | head -n1) \
|
||||
&& mv "$NODE_DIR" "$NVM_DIR/versions/node/v${NODE_VERSION}" \
|
||||
&& nvm use v${NODE_VERSION} \
|
||||
&& nvm alias default v${NODE_VERSION} \
|
||||
&& export PATH="$NVM_DIR/versions/node/v${NODE_VERSION}/bin:$PATH" \
|
||||
&& export npm_config_registry=https://registry.npmmirror.com \
|
||||
&& find /tmp/npm-cache/ -maxdepth 1 -name "npm-*.tgz" -exec npm install -g {} + \
|
||||
&& find /tmp/npm-cache/ -maxdepth 1 -name "*.tgz" ! -name "npm-*.tgz" -exec npm install -g {} + \
|
||||
# 安装 VSCode 扩展
|
||||
&& mkdir -p "/home/vscode/.vscode-server/extensions" \
|
||||
&& for ext in /tmp/vscode-cache/*.vsix; do \
|
||||
[ -f "$ext" ] || continue; \
|
||||
ext_name=$(basename "$ext" .vsix); \
|
||||
temp_dir=$(mktemp -d); \
|
||||
unzip -q "$ext" -d "$temp_dir" 2>/dev/null || true; \
|
||||
[ -d "$temp_dir/extension" ] || { rm -rf "$temp_dir"; continue; }; \
|
||||
version=$(jq -r '.version' "$temp_dir/extension/package.json" 2>/dev/null || echo "0.0.0"); \
|
||||
ext_dir="/home/vscode/.vscode-server/extensions/${ext_name}-${version}"; \
|
||||
mkdir -p "$ext_dir"; \
|
||||
cp -r "$temp_dir/extension"/* "$ext_dir/"; \
|
||||
rm -rf "$temp_dir"; \
|
||||
done \
|
||||
# 安装 Jujutsu (jj) 版本控制工具
|
||||
&& jj_tar=$(ls /tmp/jj-cache/jj-*.tar.gz 2>/dev/null | head -n1) \
|
||||
&& if [ -n "$jj_tar" ]; then \
|
||||
mkdir -p /tmp/jj-extract \
|
||||
&& tar -xzf "$jj_tar" -C /tmp/jj-extract \
|
||||
&& cp /tmp/jj-extract/jj "$UV_BIN/jj" \
|
||||
&& chmod +x "$UV_BIN/jj" \
|
||||
&& jj --version; \
|
||||
fi
|
||||
|
||||
# 将 Node.js 加入镜像级 PATH,确保非交互式进程也能找到 node/npm
|
||||
ENV PATH="/home/vscode/.nvm/versions/node/v${NODE_VERSION}/bin:${PATH}"
|
||||
|
||||
# ============================================================================
|
||||
# 阶段 4/4: 配置用户 shell 环境(.bashrc)
|
||||
# ============================================================================
|
||||
FROM dev_tools_3_4 AS shell_env_4_4
|
||||
|
||||
SHELL ["/bin/bash", "-e", "-u", "-o", "pipefail", "-c"]
|
||||
|
||||
# 修复 /usr/local/bin/code 包装脚本
|
||||
USER root
|
||||
COPY .devcontainer/scripts/code-wrapper /usr/local/bin/code
|
||||
RUN chmod +x /usr/local/bin/code
|
||||
|
||||
# 使用模板文件替代逐行 echo,通过 sed 替换占位符
|
||||
COPY .devcontainer/templates/bashrc.tail.sh /tmp/bashrc.tail.sh
|
||||
RUN sed "s|{{NODE_PATH}}|/home/vscode/.nvm/versions/node/v${NODE_VERSION}/bin|g; \
|
||||
s|{{BIN_PATH}}|${BIN_PATH}|g" \
|
||||
/tmp/bashrc.tail.sh >> /home/vscode/.bashrc \
|
||||
&& rm -f /tmp/bashrc.tail.sh \
|
||||
&& chown vscode:vscode /home/vscode/.bashrc
|
||||
USER vscode
|
||||
RUN mkdir -p /home/vscode/.ssh /home/vscode/.claude \
|
||||
&& chmod 700 /home/vscode/.ssh
|
||||
|
||||
CMD ["sleep", "infinity"]
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "team",
|
||||
"service": "app",
|
||||
"remoteUser": "vscode",
|
||||
"updateRemoteUserUID": false,
|
||||
"workspaceFolder": "/workspace",
|
||||
"dockerComposeFile": [
|
||||
"docker-compose.yml",
|
||||
"docker-compose.display.yml"
|
||||
],
|
||||
"shutdownAction": "none",
|
||||
"forwardPorts": [],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"anthropic.claude-code",
|
||||
"bierner.markdown-mermaid",
|
||||
"charliermarsh.ruff",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"github.copilot",
|
||||
"github.copilot-chat",
|
||||
"humao.rest-client",
|
||||
"james-yu.latex-workshop",
|
||||
"marp-team.marp-vscode",
|
||||
"ms-ceintl.vscode-language-pack-zh-hans",
|
||||
"ms-playwright.playwright",
|
||||
"ms-python.debugpy",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-vscode-remote.remote-containers",
|
||||
"ms-vscode-remote.remote-ssh",
|
||||
"mutantdino.resourcemonitor",
|
||||
"redhat.vscode-yaml",
|
||||
"shd101wyy.markdown-preview-enhanced",
|
||||
"tamasfe.even-better-toml",
|
||||
"tomoki1207.pdf",
|
||||
"yzane.markdown-pdf",
|
||||
"yzhang.markdown-all-in-one"
|
||||
],
|
||||
"settings": {
|
||||
"extensions.autoUpdate": "false",
|
||||
"extensions.autoCheckUpdates": false,
|
||||
"remote.autoForwardPorts": false,
|
||||
"remote.autoForwardPortsSource": "output",
|
||||
"resmon.show.battery": false,
|
||||
"resmon.show.cpufreq": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"bash": {
|
||||
"path": "bash",
|
||||
"icon": "terminal-bash"
|
||||
},
|
||||
"zsh": {
|
||||
"path": "zsh"
|
||||
}
|
||||
},
|
||||
"python.terminal.activateEnvironment": true,
|
||||
"markdown-preview-enhanced.chromePath": "/usr/bin/google-chrome-stable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "chmod +x ./.devcontainer/post-create.sh && ./.devcontainer/post-create.sh"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# X11 宿主机显示覆盖配置
|
||||
#
|
||||
# 此文件始终被 devcontainer.json 加载,通过 DISPLAY_ON_HOST 变量控制行为:
|
||||
# DISPLAY_ON_HOST=true → 挂载 X11 socket,覆盖 DISPLAY 环境变量
|
||||
# DISPLAY_ON_HOST=false → 挂载路径为空值(被忽略),不影响主配置
|
||||
#
|
||||
# 宿主机准备(Linux):
|
||||
# 1. 允许本地 X 连接: xhost +local:
|
||||
# 2. 确认 display: echo $DISPLAY
|
||||
# 3. 在 .env 中设置 DISPLAY_ON_HOST=true 和 HOST_DISPLAY=$DISPLAY
|
||||
|
||||
services:
|
||||
app:
|
||||
environment:
|
||||
DISPLAY_ON_HOST: ${DISPLAY_ON_HOST:-false}
|
||||
# 仅当 DISPLAY_ON_HOST=true 时,post-create.sh 使用此值替代 Xvfb
|
||||
HOST_DISPLAY: ${HOST_DISPLAY:-:0}
|
||||
volumes:
|
||||
# X11 socket 挂载:始终挂载,容器内脚本按需检测
|
||||
# 如果宿主机无 X11(如 macOS/Windows),此目录为空,不影响运行
|
||||
- /tmp/.X11-unix:/tmp/.host-x11
|
||||
@@ -0,0 +1,81 @@
|
||||
# Docker Compose 配置文件
|
||||
#
|
||||
# 启动前准备:
|
||||
# 1. 复制环境变量配置: cp .env.example .env
|
||||
# 2. 根据需要修改 .env 文件
|
||||
# 3. (可选)预下载构建资源: bash download-resources.sh
|
||||
#
|
||||
# 启动方式:
|
||||
# 方法 1(推荐): bash start.sh
|
||||
# 方法 2: docker compose up(需要确保 .env 文件存在)
|
||||
|
||||
services:
|
||||
app:
|
||||
env_file:
|
||||
- .env
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
args:
|
||||
CONTAINER_USER_UID: ${CONTAINER_USER_UID:-1000}
|
||||
CONTAINER_USER_GID: ${CONTAINER_USER_GID:-1000}
|
||||
PROJECT_NAME: ${PROJECT_NAME:-isos}
|
||||
DOCKER_GID: ${DOCKER_GID:-984}
|
||||
image: vscode:${PROJECT_NAME:-isos}
|
||||
container_name: ${PROJECT_NAME:-isos}
|
||||
hostname: ${PROJECT_NAME:-isos}
|
||||
extra_hosts:
|
||||
- "${PROJECT_NAME:-isos}:127.0.0.1"
|
||||
- "localhost:127.0.0.1"
|
||||
# 特权模式(可选,部分场景需要)
|
||||
privileged: true
|
||||
# 资源限制配置
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '${CONTAINER_CPUS:-4}'
|
||||
memory: ${CONTAINER_MEMORY:-8G}
|
||||
# 配置 GPU 访问:all 表示所有 GPU,也可指定具体 GPU,如 "device=0,1"
|
||||
# deploy:
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: all
|
||||
# capabilities: [gpu]
|
||||
environment:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
DOCKER_ENABLED: ${DOCKER_ENABLED:-true}
|
||||
DOCKER_GID: ${DOCKER_GID:-984}
|
||||
# 让容器可见所有 GPU
|
||||
# NVIDIA_VISIBLE_DEVICES: all
|
||||
# 启用计算和工具类能力
|
||||
# NVIDIA_DRIVER_CAPABILITIES: compute,utility
|
||||
volumes:
|
||||
- ../:/workspace:delegated
|
||||
- ${DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock
|
||||
- ${SSH_PATH:-.volumes/ssh}:/home/vscode/.ssh
|
||||
- ${CLAUDE_PATH:-.volumes/claude}:/home/vscode/.claude
|
||||
- ${JJ_CONFIG_PATH:-.volumes/jj}:/home/vscode/.config/jj
|
||||
working_dir: /workspace
|
||||
network_mode: host
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
stack:
|
||||
soft: 67108864
|
||||
hard: 67108864
|
||||
# 配置 IPC 模式
|
||||
ipc: host
|
||||
# dns:
|
||||
# - 119.29.29.29
|
||||
# - 223.5.5.5
|
||||
# - 8.8.8.8
|
||||
# - 1.1.1.1
|
||||
# - 202.96.128.143
|
||||
# - 202.96.128.68
|
||||
# - 202.96.134.133
|
||||
# - 202.96.128.86
|
||||
# - 202.96.128.166
|
||||
command: sleep infinity
|
||||
Executable
+1134
File diff suppressed because it is too large
Load Diff
Executable
+539
@@ -0,0 +1,539 @@
|
||||
#!/bin/bash
|
||||
# ==============================================================================
|
||||
# DevContainer 公共函数库
|
||||
# ==============================================================================
|
||||
|
||||
# 禁止直接执行,必须被 source
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
echo "错误: 此文件应该被 source,不能直接执行"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# 颜色代码定义
|
||||
# ============================================================================
|
||||
|
||||
readonly COLOR_RED='\033[0;31m'
|
||||
readonly COLOR_GREEN='\033[0;32m'
|
||||
readonly COLOR_YELLOW='\033[0;33m'
|
||||
readonly COLOR_BLUE='\033[0;34m'
|
||||
readonly COLOR_PURPLE='\033[0;35m'
|
||||
readonly COLOR_CYAN='\033[0;36m'
|
||||
readonly COLOR_GRAY='\033[0;90m'
|
||||
readonly COLOR_BOLD='\033[1m'
|
||||
readonly COLOR_RESET='\033[0m'
|
||||
|
||||
# ============================================================================
|
||||
# 状态符号函数
|
||||
# ============================================================================
|
||||
|
||||
status_ok() {
|
||||
echo -e "${COLOR_GREEN}✅${COLOR_RESET}"
|
||||
}
|
||||
|
||||
status_warn() {
|
||||
echo -e "${COLOR_YELLOW}⚠️${COLOR_RESET}"
|
||||
}
|
||||
|
||||
status_miss() {
|
||||
echo -e "${COLOR_RED}❌${COLOR_RESET}"
|
||||
}
|
||||
|
||||
status_info() {
|
||||
echo -e "${COLOR_BLUE}ℹ️${COLOR_RESET}"
|
||||
}
|
||||
|
||||
status_fail() {
|
||||
echo -e "${COLOR_RED}🔴${COLOR_RESET}"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 带颜色的文本函数
|
||||
# ============================================================================
|
||||
|
||||
text_green() {
|
||||
echo -e "${COLOR_GREEN}${1}${COLOR_RESET}"
|
||||
}
|
||||
|
||||
text_yellow() {
|
||||
echo -e "${COLOR_YELLOW}${1}${COLOR_RESET}"
|
||||
}
|
||||
|
||||
text_red() {
|
||||
echo -e "${COLOR_RED}${1}${COLOR_RESET}"
|
||||
}
|
||||
|
||||
text_blue() {
|
||||
echo -e "${COLOR_BLUE}${1}${COLOR_RESET}"
|
||||
}
|
||||
|
||||
text_cyan() {
|
||||
echo -e "${COLOR_CYAN}${1}${COLOR_RESET}"
|
||||
}
|
||||
|
||||
text_gray() {
|
||||
echo -e "${COLOR_GRAY}${1}${COLOR_RESET}"
|
||||
}
|
||||
|
||||
text_bold() {
|
||||
echo -e "${COLOR_BOLD}${1}${COLOR_RESET}"
|
||||
}
|
||||
|
||||
text_purple() {
|
||||
echo -e "${COLOR_PURPLE}${1}${COLOR_RESET}"
|
||||
}
|
||||
|
||||
text_reset() {
|
||||
echo -e "${COLOR_RESET}"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 输出格式函数
|
||||
# ============================================================================
|
||||
|
||||
print_header() {
|
||||
local title="$1"
|
||||
echo ""
|
||||
echo "$(text_bold '═══════════════════════════════════════════════════════════')"
|
||||
echo "$(text_bold "${title}")"
|
||||
echo "$(text_bold '═══════════════════════════════════════════════════════════')"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_section() {
|
||||
local title="$1"
|
||||
echo ""
|
||||
echo "$(text_bold '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')"
|
||||
echo "$(text_bold "${title}")"
|
||||
echo "$(text_bold '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')"
|
||||
echo ""
|
||||
}
|
||||
|
||||
print_step() {
|
||||
local step="$1"
|
||||
local desc="$2"
|
||||
echo "[${step}] ${desc}"
|
||||
}
|
||||
|
||||
print_status() {
|
||||
local status="$1"
|
||||
local message="$2"
|
||||
printf " %s %s\n" "${status}" "${message}"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 辅助函数
|
||||
# ============================================================================
|
||||
|
||||
# 追加配置到文件(如果不存在标记)
|
||||
append_config() {
|
||||
local file="$1"
|
||||
local marker="$2"
|
||||
local content="$3"
|
||||
|
||||
grep -q "$marker" "$file" 2>/dev/null || echo "$content" >> "$file"
|
||||
}
|
||||
|
||||
# 检查工具是否安装
|
||||
check_tool() {
|
||||
local name="$1"
|
||||
local cmd="$2"
|
||||
local version_cmd="${3:-$cmd --version}"
|
||||
|
||||
if command -v "$cmd" &>/dev/null; then
|
||||
echo " $(status_ok) ${name}: $($version_cmd 2>/dev/null | head -n1)"
|
||||
else
|
||||
echo " $(status_miss) ${name}"
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查配置文件
|
||||
check_config() {
|
||||
local file="$1"
|
||||
local pattern="$2"
|
||||
local desc="$3"
|
||||
|
||||
if [ -f "$file" ] && grep -q "$pattern" "$file" 2>/dev/null; then
|
||||
echo " $(status_ok) ${desc}"
|
||||
else
|
||||
local status="配置异常"
|
||||
[ ! -f "$file" ] && status="文件不存在"
|
||||
echo " $(status_warn) ${desc} ($(text_red "$status"))"
|
||||
fi
|
||||
}
|
||||
|
||||
# 测试 URL 可用性
|
||||
test_url() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local status="$(status_fail)"
|
||||
|
||||
wget -q --spider --connect-timeout=3 --timeout=5 "$url" 2>/dev/null && status="$(status_ok)"
|
||||
echo " ${name}: ${status}"
|
||||
}
|
||||
|
||||
# 计算字符串显示宽度(中文字符算2个宽度)
|
||||
calc_width() {
|
||||
local str=$1
|
||||
local width=0
|
||||
local char
|
||||
|
||||
while read -r -n1 char; do
|
||||
local code
|
||||
code=$(printf '%d' "'${char}" 2>/dev/null || echo 0)
|
||||
((width += (code > 127 ? 2 : 1)))
|
||||
done <<< "$str"
|
||||
echo "$width"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 文件验证函数
|
||||
# ============================================================================
|
||||
|
||||
# 检查文件是否存在且非空
|
||||
file_exists() {
|
||||
[ -f "$1" ] && [ -s "$1" ]
|
||||
}
|
||||
|
||||
# 检查通配符文件是否存在
|
||||
glob_exists() {
|
||||
local file
|
||||
file=$(ls $1 2>/dev/null | head -n1)
|
||||
[ -n "${file}" ] && [ -f "${file}" ] && [ -s "${file}" ]
|
||||
}
|
||||
|
||||
# 检查目录是否有效(非空)
|
||||
dir_valid() {
|
||||
[ -d "$1" ] && [ "$(find "$1" -maxdepth 1 -mindepth 1 -not -name ".git" 2>/dev/null | wc -l)" -gt 0 ]
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 下载函数(带重试和校验)
|
||||
# ============================================================================
|
||||
|
||||
# 计算 SHA256 校验和
|
||||
compute_sha256() {
|
||||
local file=$1
|
||||
if [ -f "${file}" ]; then
|
||||
sha256sum "${file}" 2>/dev/null | cut -d' ' -f1
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# 从 GitHub releases 获取预期校验和
|
||||
get_github_release_checksum() {
|
||||
local repo=$1
|
||||
local version=$2
|
||||
local filename=$3
|
||||
local checksums_url="https://github.com/${repo}/releases/download/${version}/checksums.txt"
|
||||
|
||||
local temp_checksums
|
||||
temp_checksums=$(mktemp)
|
||||
|
||||
if wget -q --timeout=30 -O "${temp_checksums}" "${checksums_url}" 2>/dev/null; then
|
||||
local checksum
|
||||
checksum=$(grep "${filename}" "${temp_checksums}" 2>/dev/null | awk '{print $1}' | head -n1)
|
||||
rm -f "${temp_checksums}"
|
||||
echo "${checksum}"
|
||||
else
|
||||
rm -f "${temp_checksums}"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# 从 SHASUMS256.txt 文件获取校验和(用于 Node.js)
|
||||
get_shasums256_checksum() {
|
||||
local shasums_url=$1
|
||||
local filename=$2
|
||||
|
||||
local temp_shasums
|
||||
temp_shasums=$(mktemp)
|
||||
|
||||
if wget -q --timeout=30 -O "${temp_shasums}" "${shasums_url}" 2>/dev/null; then
|
||||
local checksum
|
||||
checksum=$(grep "${filename}" "${temp_shasums}" 2>/dev/null | awk '{print $1}' | head -n1)
|
||||
rm -f "${temp_shasums}"
|
||||
echo "${checksum}"
|
||||
else
|
||||
rm -f "${temp_shasums}"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# 验证文件完整性
|
||||
verify_file_integrity() {
|
||||
local file=$1
|
||||
local expected_checksum=$2
|
||||
local min_size=${3:-1024}
|
||||
|
||||
if [ ! -f "${file}" ] || [ ! -s "${file}" ]; then
|
||||
printf " %s 文件不存在或为空: %s\n" "$(status_fail)" "$(basename "${file}")"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local file_size
|
||||
file_size=$(stat -c%s "${file}" 2>/dev/null || echo 0)
|
||||
if [ -n "${min_size}" ] && [ "${file_size}" -lt "${min_size}" ]; then
|
||||
printf " %s 文件过小: %s 字节 (最小: %s)\n" "$(status_fail)" "${file_size}" "${min_size}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "${expected_checksum}" ]; then
|
||||
local actual_checksum
|
||||
actual_checksum=$(compute_sha256 "${file}")
|
||||
if [ "${actual_checksum}" != "${expected_checksum}" ]; then
|
||||
printf " %s 校验和不匹配\n" "$(status_fail)"
|
||||
printf " 预期: %s\n" "${expected_checksum}"
|
||||
printf " 实际: %s\n" "${actual_checksum}"
|
||||
return 1
|
||||
fi
|
||||
printf " %s 校验和验证通过\n" "$(status_ok)"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# 下载文件(带重试)
|
||||
download_file() {
|
||||
local url=$1
|
||||
local output_file=$2
|
||||
local desc=$3
|
||||
local expected_checksum=$4
|
||||
local min_size=${5:-1024}
|
||||
local max_retries=3
|
||||
local retry_count=0
|
||||
|
||||
if file_exists "${output_file}"; then
|
||||
if [ -n "${expected_checksum}" ]; then
|
||||
if verify_file_integrity "${output_file}" "${expected_checksum}" "${min_size}"; then
|
||||
print_status_skip "$(basename "${output_file}")" "已验证"
|
||||
return 0
|
||||
else
|
||||
printf " %s 文件损坏,重新下载...\n" "$(status_warn)"
|
||||
rm -f "${output_file}"
|
||||
fi
|
||||
else
|
||||
if verify_file_integrity "${output_file}" "" "${min_size}"; then
|
||||
print_status_skip "$(basename "${output_file}")"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
while [ $retry_count -lt $max_retries ]; do
|
||||
print_status_get "${desc}"
|
||||
printf " %s 下载地址: %s\n" "$(status_info)" "${url}"
|
||||
|
||||
local wget_exit_code=0
|
||||
if [ -f "${output_file}" ] && [ -s "${output_file}" ]; then
|
||||
# 文件存在但校验失败,使用断点续传
|
||||
wget -c --tries=3 --waitretry=2 --show-progress \
|
||||
--connect-timeout=10 --timeout=300 \
|
||||
-O "${output_file}" "${url}" 2>&1 || wget_exit_code=$?
|
||||
else
|
||||
wget --tries=3 --waitretry=2 --show-progress \
|
||||
--connect-timeout=10 --timeout=300 \
|
||||
-O "${output_file}" "${url}" 2>&1 || wget_exit_code=$?
|
||||
fi
|
||||
|
||||
if [ ${wget_exit_code} -eq 0 ]; then
|
||||
if verify_file_integrity "${output_file}" "${expected_checksum}" "${min_size}"; then
|
||||
print_status_ok "${desc}"
|
||||
return 0
|
||||
else
|
||||
((retry_count++))
|
||||
if [ $retry_count -lt $max_retries ]; then
|
||||
print_status_retry "下载" "${retry_count}"
|
||||
sleep 2
|
||||
rm -f "${output_file}"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
((retry_count++))
|
||||
if [ $retry_count -lt $max_retries ]; then
|
||||
print_status_retry "下载失败 (wget=${wget_exit_code})" "${retry_count}"
|
||||
sleep 2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
print_status_fail "${desc}" "已重试 ${max_retries} 次"
|
||||
rm -f "${output_file}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 克隆仓库
|
||||
clone_repo() {
|
||||
local url=$1
|
||||
local dest_dir=$2
|
||||
local branch=$3
|
||||
local desc=$4
|
||||
|
||||
if dir_valid "${dest_dir}"; then
|
||||
print_status_skip "${desc}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -rf "${dest_dir}" 2>/dev/null
|
||||
printf " %s %s\n" "$(text_cyan "⟳ ")" "${desc}"
|
||||
|
||||
local clone_args=("--depth=1")
|
||||
[ -n "${branch}" ] && clone_args+=("--branch" "${branch}")
|
||||
|
||||
if git clone "${clone_args[@]}" "${url}" "${dest_dir}" && dir_valid "${dest_dir}"; then
|
||||
rm -rf "${dest_dir}/.git"
|
||||
print_status_ok "${desc}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_status_fail "${desc}"
|
||||
rm -rf "${dest_dir}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# VSCode 扩展函数
|
||||
# ============================================================================
|
||||
|
||||
# 从 .vsix 文件中提取扩展版本
|
||||
get_vscode_extension_version() {
|
||||
local vsix_file=$1
|
||||
if [ -f "${vsix_file}" ]; then
|
||||
unzip -p "${vsix_file}" "extension/package.json" 2>/dev/null | \
|
||||
grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | \
|
||||
cut -d'"' -f4 | head -n1
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# 从 Marketplace API 获取扩展的最新版本
|
||||
get_vscode_latest_version() {
|
||||
local extension_id=$1
|
||||
local publisher
|
||||
local extension_name
|
||||
|
||||
publisher=${extension_id%%.*}
|
||||
extension_name=${extension_id#*.}
|
||||
|
||||
local api_url="https://marketplace.visualstudio.com/items?itemName=${extension_id}"
|
||||
|
||||
# 从 Marketplace 页面提取版本号
|
||||
wget -q --timeout=30 -U "Mozilla/5.0" -O - "${api_url}" 2>/dev/null | \
|
||||
grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | \
|
||||
cut -d'"' -f4 | head -n1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# NPM 包版本函数
|
||||
# ============================================================================
|
||||
|
||||
# 从 .tgz 文件中提取 npm 包版本
|
||||
get_npm_package_version() {
|
||||
local tgz_file=$1
|
||||
if [ -f "${tgz_file}" ]; then
|
||||
# npm pack 生成的文件结构为 package/package.json
|
||||
tar -xzf "${tgz_file}" -O package/package.json 2>/dev/null | \
|
||||
grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | \
|
||||
cut -d'"' -f4 | head -n1
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# 从 npm registry 获取包的最新版本
|
||||
get_npm_latest_version() {
|
||||
local package_name=$1
|
||||
local registry=${2:-"https://registry.npmmirror.com"}
|
||||
|
||||
# 使用 npm view 命令获取版本
|
||||
npm view "${package_name}" version --registry "${registry}" 2>/dev/null | head -n1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 下载状态输出函数
|
||||
# ============================================================================
|
||||
|
||||
# 统一的下载状态输出
|
||||
print_status_check() {
|
||||
local item=$1
|
||||
printf " %s %s\n" "$(status_info)" "${item}"
|
||||
}
|
||||
|
||||
print_status_get() {
|
||||
local item=$1
|
||||
printf " %s %s\n" "$(text_cyan "↓ ")" "${item}"
|
||||
}
|
||||
|
||||
print_status_skip() {
|
||||
local item=$1
|
||||
local version=${2:-""}
|
||||
if [ -n "${version}" ]; then
|
||||
printf " %s %s (版本: %s)\n" "$(status_ok)" "${item}" "${version}"
|
||||
else
|
||||
printf " %s %s\n" "$(status_ok)" "${item}"
|
||||
fi
|
||||
}
|
||||
|
||||
print_status_update() {
|
||||
local item=$1
|
||||
local old_version=$2
|
||||
local new_version=$3
|
||||
printf " %s %s: %s → %s\n" "$(text_yellow "↻ ")" "${item}" "${old_version}" "${new_version}"
|
||||
}
|
||||
|
||||
print_status_ok() {
|
||||
local item=$1
|
||||
printf " %s %s\n" "$(status_ok)" "${item}"
|
||||
}
|
||||
|
||||
print_status_fail() {
|
||||
local item=$1
|
||||
local reason=${2:-""}
|
||||
if [ -n "${reason}" ]; then
|
||||
printf " %s %s (%s)\n" "$(status_fail)" "${item}" "${reason}"
|
||||
else
|
||||
printf " %s %s\n" "$(status_fail)" "${item}"
|
||||
fi
|
||||
}
|
||||
|
||||
print_status_warn() {
|
||||
local item=$1
|
||||
local message=${2:-""}
|
||||
if [ -n "${message}" ]; then
|
||||
printf " %s %s: %s\n" "$(status_warn)" "${item}" "${message}"
|
||||
else
|
||||
printf " %s %s\n" "$(status_warn)" "${item}"
|
||||
fi
|
||||
}
|
||||
|
||||
print_status_retry() {
|
||||
local item=$1
|
||||
local count=$2
|
||||
printf " %s %s - 第 %s 次重试...\n" "$(status_warn)" "${item}" "${count}"
|
||||
}
|
||||
|
||||
print_status_info() {
|
||||
local message=$1
|
||||
printf " %s %s\n" "$(status_info)" "${message}"
|
||||
}
|
||||
|
||||
print_step_extra() {
|
||||
local label=${1:-"+1"}
|
||||
printf "%s " "$(text_yellow "[${label}]")"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 导出所有函数
|
||||
# ============================================================================
|
||||
|
||||
export -f status_ok status_warn status_miss status_info status_fail
|
||||
export -f text_green text_yellow text_red text_blue text_cyan text_gray text_bold text_purple text_reset
|
||||
export -f print_header print_section print_step print_status
|
||||
export -f append_config check_tool check_config test_url calc_width
|
||||
export -f file_exists glob_exists dir_valid
|
||||
export -f compute_sha256 get_github_release_checksum get_shasums256_checksum verify_file_integrity download_file clone_repo
|
||||
export -f get_vscode_extension_version get_vscode_latest_version
|
||||
export -f get_npm_package_version get_npm_latest_version
|
||||
export -f print_status_check print_status_get print_status_skip print_status_update print_status_ok print_status_fail print_status_warn print_status_retry print_step_extra
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {},
|
||||
"provider": {
|
||||
"zhipuai-coding-plan": {}
|
||||
},
|
||||
"model": "zhipuai-coding-plan/glm-5.1",
|
||||
"small_model": "zhipuai-coding-plan/glm-4.5-air"
|
||||
}
|
||||
Executable
+1034
File diff suppressed because it is too large
Load Diff
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
get_in_path_except_current() {
|
||||
which -a "$1" | grep -A1 "$0" | grep -v "$0"
|
||||
}
|
||||
|
||||
code="$(get_in_path_except_current code)"
|
||||
|
||||
if [ -n "$code" ]; then
|
||||
exec "$code" "$@"
|
||||
elif [ "$(command -v code-insiders)" ]; then
|
||||
exec code-insiders "$@"
|
||||
fi
|
||||
|
||||
# Fallback: search for vscode-server remote-cli (DevContainer 环境)
|
||||
for dir in "${HOME}/.vscode-server/bin"/*/bin/remote-cli \
|
||||
"/root/.vscode-server/bin"/*/bin/remote-cli \
|
||||
"${HOME}/.cursor-server/bin"/*/bin/remote-cli; do
|
||||
if [ -x "$dir/code" ]; then
|
||||
exec "$dir/code" "$@"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "code or code-insiders is not installed" >&2
|
||||
exit 127
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"numStartups": 80,
|
||||
"installMethod": "global",
|
||||
"hasSeenTasksHint": true,
|
||||
"autoInstallIdeExtension": false,
|
||||
"cachedStatsigGates": {
|
||||
"tengu_prompt_suggestion": false
|
||||
},
|
||||
"mcpServers": {
|
||||
"zai-mcp-server": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@z_ai/mcp-server"
|
||||
],
|
||||
"env": {
|
||||
"Z_AI_MODE": "ZHIPU",
|
||||
"Z_AI_API_KEY": "{{your_api_key}}"
|
||||
}
|
||||
},
|
||||
"web-search-prime": {
|
||||
"type": "http",
|
||||
"url": "https://open.bigmodel.cn/api/mcp/web_search_prime/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{your_api_key}}"
|
||||
}
|
||||
},
|
||||
"web-reader": {
|
||||
"type": "http",
|
||||
"url": "https://open.bigmodel.cn/api/mcp/web_reader/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{your_api_key}}"
|
||||
}
|
||||
},
|
||||
"zread": {
|
||||
"type": "http",
|
||||
"url": "https://open.bigmodel.cn/api/mcp/zread/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer {{your_api_key}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"firstStartTime": "2026-01-01T01:02:03.004Z",
|
||||
"unpinOpus47LaunchEffort": true,
|
||||
"opusProMigrationComplete": true,
|
||||
"sonnet1m45MigrationComplete": true,
|
||||
"thinkingMigrationComplete": true,
|
||||
"hasCompletedOnboarding": true,
|
||||
"migrationVersion": 11,
|
||||
"lastReleaseNotesSeen": "2.1.112",
|
||||
"hasIdeOnboardingBeenShown": {
|
||||
"vscode": true
|
||||
},
|
||||
"officialMarketplaceAutoInstallAttempted": true,
|
||||
"officialMarketplaceAutoInstalled": true,
|
||||
"showSpinnerTree": false
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
# ============================================================================
|
||||
# DevContainer .bashrc 追加配置
|
||||
# 由 Dockerfile 通过 sed 替换占位符后追加到 ~/.bashrc
|
||||
# ============================================================================
|
||||
|
||||
# 本地二进制路径(必须优先于 NVM)
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
|
||||
# NVM 配置
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
|
||||
# Node.js 镜像配置
|
||||
export NVM_NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node
|
||||
export npm_config_registry=https://registry.npmmirror.com
|
||||
export YARN_REGISTRY=https://registry.npmmirror.com
|
||||
export ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
|
||||
# Python/UV 配置
|
||||
export PATH="{{NODE_PATH}}:{{BIN_PATH}}:$PATH"
|
||||
export UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
export UV_LINK_MODE=copy
|
||||
export PYTHONUNBUFFERED=1
|
||||
alias uv-python='uv run python'
|
||||
alias uv-pip='uv run pip'
|
||||
|
||||
# 语言和区域设置
|
||||
export LC_ALL=zh_CN.UTF-8
|
||||
export LC_TIME=zh_CN.UTF-8
|
||||
export LANG=zh_CN.UTF-8
|
||||
|
||||
# 其他配置
|
||||
export COREPACK_ENABLE_DOWNLOAD_PROMPT=false
|
||||
export EDITOR=vim
|
||||
export VISUAL=vim
|
||||
export LESSCHARSET=utf-8
|
||||
|
||||
# Jujutsu (jj) 补全
|
||||
if command -v jj >/dev/null 2>&1; then
|
||||
source <(jj util completion bash 2>/dev/null)
|
||||
fi
|
||||
|
||||
# 别名配置
|
||||
alias ll='ls -alhF --color=auto'
|
||||
alias date='date "+%F %T %p %A %B"'
|
||||
export TIME_STYLE='+%Y-%m-%d %H:%M:%S'
|
||||
|
||||
# Python 路径
|
||||
export PYTHONPATH=/workspace:$PYTHONPATH
|
||||
|
||||
# NVM IO.js 镜像
|
||||
export NVM_IOJS_ORG_MIRROR="https://iojs.org/dist"
|
||||
|
||||
# Claude Code 启动函数(参数透传至 runcc.sh,支持 -n/--name 走 /rename 持久化)
|
||||
cc() { {{BIN_PATH}}/runcc.sh "$@"; }
|
||||
|
||||
# 模型快捷方式: cc51, cc51 -n 后端, cc -n 后端 51 ...
|
||||
alias cc45='cc 45'
|
||||
alias cc45a='cc 45a'
|
||||
alias cc46='cc 46'
|
||||
alias cc47='cc 47'
|
||||
alias cc5='cc 5'
|
||||
alias cc5t='cc 5t'
|
||||
alias cc51='cc 51'
|
||||
|
||||
# OpenCode 启动函数(参数透传至 runoc.sh)
|
||||
oc() { {{BIN_PATH}}/runoc.sh "$@"; }
|
||||
|
||||
# 模型快捷方式: oc51, oc 51 ...
|
||||
alias oc45='oc 45'
|
||||
alias oc45a='oc 45a'
|
||||
alias oc45f='oc 45f'
|
||||
alias oc46='oc 46'
|
||||
alias oc46v='oc 46v'
|
||||
alias oc46vf='oc 46vf'
|
||||
alias oc47='oc 47'
|
||||
alias oc5='oc 5'
|
||||
alias oc5t='oc 5t'
|
||||
alias oc51='oc 51'
|
||||
|
||||
# tmux pane 级模型继承:从 pane option 读取模型配置(由 runcc.sh 写入)
|
||||
# 覆盖 session 级环境变量,实现同 session 不同 pane 的模型隔离
|
||||
if [ -n "${TMUX:-}" ]; then
|
||||
_model="$(tmux show-option -pv @anthropic_model 2>/dev/null || true)"
|
||||
if [ -n "$_model" ]; then
|
||||
export ANTHROPIC_DEFAULT_HAIKU_MODEL="$_model"
|
||||
export ANTHROPIC_DEFAULT_SONNET_MODEL="$_model"
|
||||
export ANTHROPIC_DEFAULT_OPUS_MODEL="$_model"
|
||||
fi
|
||||
unset _model
|
||||
|
||||
_oc_model="$(tmux show-option -pv @opencode_model 2>/dev/null || true)"
|
||||
if [ -n "$_oc_model" ]; then
|
||||
export OPENCODE_MODEL="$_oc_model"
|
||||
fi
|
||||
unset _oc_model
|
||||
fi
|
||||
|
||||
# Docker 组提示(仅在需要时显示)
|
||||
if [ "${DOCKER_ENABLED:-true}" = "true" ] && [ -S /var/run/docker.sock ] && ! groups | grep -q docker; then
|
||||
export DOCKER_GROUP_REQUIRED=1
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# VCS 感知提示符(覆盖 devcontainer 默认,支持 jj bookmark 显示)
|
||||
# ============================================================================
|
||||
# 在 bashrc 加载时检测 jj 仓库(只需一次,避免每次提示都调用 jj root)
|
||||
__JJ_REPO=0
|
||||
if command -v jj >/dev/null 2>&1 && jj root >/dev/null 2>&1; then
|
||||
__JJ_REPO=1
|
||||
fi
|
||||
|
||||
__vcs_prompt() {
|
||||
local userpart='`export XIT=$? \
|
||||
&& [ ! -z "${GITHUB_USER:-}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER:-} " || echo -n "\[\033[0;32m\]\u " \
|
||||
&& [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`'
|
||||
local vcsbranch='`\
|
||||
if [ "${__JJ_REPO:-0}" = "1" ]; then \
|
||||
BRANCH=$(jj bookmark list -r @- 2>/dev/null | sed "s/[*:].*//" | head -1); \
|
||||
if [ -z "${BRANCH:-}" ]; then \
|
||||
BRANCH=$(jj log -r "@-" --no-graph -T "change_id.short()" 2>/dev/null); \
|
||||
fi; \
|
||||
if [ -n "${BRANCH:-}" ]; then \
|
||||
echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}\[\033[0;36m\]) "; \
|
||||
fi; \
|
||||
else \
|
||||
BRANCH="$(git --no-optional-locks symbolic-ref --short HEAD 2>/dev/null || git --no-optional-locks rev-parse --short HEAD 2>/dev/null)"; \
|
||||
if [ -n "${BRANCH:-}" ]; then \
|
||||
echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}\[\033[0;36m\]) "; \
|
||||
fi; \
|
||||
fi`'
|
||||
local lightblue='\[\033[1;34m\]'
|
||||
local removecolor='\[\033[0m\]'
|
||||
PS1="${userpart} ${lightblue}\w ${vcsbranch}${removecolor}\$ "
|
||||
}
|
||||
__vcs_prompt
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# Claude Code 启动脚本,支持通过参数选择 GLM 模型版本
|
||||
#
|
||||
# 固定版本: 2.1.112 (来自 /workspace/.devcontainer/.cache/npm/anthropic-ai-claude-code-2.1.112.tgz)
|
||||
# 注意: 不再执行自动更新,以保持版本稳定
|
||||
|
||||
SETTINGS_FILE="/workspace/.devcontainer/.volumes/claude/settings.json"
|
||||
|
||||
# 参数解析: runcc.sh [--name <名称>] [模型]
|
||||
SESSION_NAME=""
|
||||
MODEL_KEY=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--name|-n)
|
||||
[ $# -ge 2 ] || { echo "错误: --name 需要参数" >&2; exit 1; }
|
||||
SESSION_NAME="$2"; shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
echo "用法: runcc.sh [--name <名称>] [模型]"
|
||||
echo ""
|
||||
echo "选项:"
|
||||
echo " --name, -n <名称> Claude Code 会话显示名称"
|
||||
echo ""
|
||||
echo "模型: 45|45a|46|47|5|5t|51 (默认 51)"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
MODEL_KEY="$1"; shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
MODEL_KEY="${MODEL_KEY:-51}" # 默认使用 glm-5.1
|
||||
case "$MODEL_KEY" in
|
||||
45) MODEL="glm-4.5" ;;
|
||||
45a) MODEL="glm-4.5-air" ;;
|
||||
46) MODEL="glm-4.6" ;;
|
||||
47) MODEL="glm-4.7" ;;
|
||||
5) MODEL="glm-5" ;;
|
||||
5t) MODEL="glm-5-turbo" ;;
|
||||
51) MODEL="glm-5.1" ;;
|
||||
*)
|
||||
echo "未知模型: $MODEL_KEY (可选: 45|45a|46|47|5|5t|51)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 从 settings.json 中移除模型相关配置(避免覆盖 shell 环境变量)
|
||||
if command -v jq &>/dev/null && [ -f "$SETTINGS_FILE" ]; then
|
||||
jq 'del(.env.ANTHROPIC_DEFAULT_OPUS_MODEL, .env.ANTHROPIC_DEFAULT_HAIKU_MODEL, .env.ANTHROPIC_DEFAULT_SONNET_MODEL)' \
|
||||
"$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" \
|
||||
&& mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE"
|
||||
fi
|
||||
|
||||
# ── 模型环境变量(三个层级统一为同一模型)────────────────
|
||||
export ANTHROPIC_DEFAULT_HAIKU_MODEL="$MODEL"
|
||||
export ANTHROPIC_DEFAULT_SONNET_MODEL="$MODEL"
|
||||
export ANTHROPIC_DEFAULT_OPUS_MODEL="$MODEL"
|
||||
|
||||
# ── tmux 模型传播(确保子 pane 能获取正确模型)─────────
|
||||
if [ -n "${TMUX:-}" ]; then
|
||||
SESSION_ID="$(tmux display-message -p '#{session_id}')"
|
||||
|
||||
# 层级 2: session 级环境变量(不同 session 隔离)
|
||||
tmux setenv -t "$SESSION_ID" ANTHROPIC_DEFAULT_OPUS_MODEL "$MODEL"
|
||||
tmux setenv -t "$SESSION_ID" ANTHROPIC_DEFAULT_HAIKU_MODEL "$MODEL"
|
||||
tmux setenv -t "$SESSION_ID" ANTHROPIC_DEFAULT_SONNET_MODEL "$MODEL"
|
||||
|
||||
# 层级 3: pane 级选项(同 session 不同 pane 隔离)
|
||||
# .bashrc 会在新 shell 启动时读取此选项,覆盖 session 级设置
|
||||
tmux set-option -p @anthropic_model "$MODEL"
|
||||
fi
|
||||
|
||||
# 构建启动参数
|
||||
CC_ARGS=(--ide --dangerously-skip-permissions --allow-dangerously-skip-permissions)
|
||||
|
||||
# --name 不可靠(不写 custom-title/agent-name JSONL 记录),
|
||||
# 改用 /rename 斜杠命令作为初始提示,确保名称正确持久化
|
||||
if [ -n "$SESSION_NAME" ]; then
|
||||
INITIAL_PROMPT="/rename $SESSION_NAME"
|
||||
fi
|
||||
|
||||
echo "启动 Claude Code (模型: $MODEL${SESSION_NAME:+, 会话: $SESSION_NAME})"
|
||||
claude "${CC_ARGS[@]}" ${INITIAL_PROMPT:+"$INITIAL_PROMPT"}
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/bash
|
||||
# OpenCode 启动脚本,支持通过参数选择 GLM 模型版本
|
||||
#
|
||||
# 用法: runoc.sh [模型]
|
||||
#
|
||||
# 模型: 45|45a|45f|46|46v|46vf|47|5|5t|51 (默认 51)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_CONFIG="/workspace/opencode.json"
|
||||
GLOBAL_CONFIG="$HOME/.config/opencode/opencode.json"
|
||||
|
||||
# 参数解析
|
||||
MODEL_KEY=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h|--help)
|
||||
echo "用法: runoc.sh [模型]"
|
||||
echo ""
|
||||
echo "模型: 45|45a|45f|46|46v|46vf|47|5|5t|51 (默认 51)"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
MODEL_KEY="$1"; shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
MODEL_KEY="${MODEL_KEY:-51}"
|
||||
case "$MODEL_KEY" in
|
||||
45) MODEL="zhipuai-coding-plan/glm-4.5" ;;
|
||||
45a) MODEL="zhipuai-coding-plan/glm-4.5-air" ;;
|
||||
45f) MODEL="zhipuai-coding-plan/glm-4.5-flash" ;;
|
||||
46) MODEL="zhipuai-coding-plan/glm-4.6" ;;
|
||||
46v) MODEL="zhipuai-coding-plan/glm-4.6v" ;;
|
||||
46vf) MODEL="zhipuai-coding-plan/glm-4.6v-flash" ;;
|
||||
47) MODEL="zhipuai-coding-plan/glm-4.7" ;;
|
||||
5) MODEL="zhipuai-coding-plan/glm-5" ;;
|
||||
5t) MODEL="zhipuai-coding-plan/glm-5-turbo" ;;
|
||||
51) MODEL="zhipuai-coding-plan/glm-5.1" ;;
|
||||
*)
|
||||
echo "未知模型: $MODEL_KEY (可选: 45|45a|45f|46|46v|46vf|47|5|5t|51)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── tmux 模型传播(确保子 pane 能获取正确模型)─────────
|
||||
if [ -n "${TMUX:-}" ]; then
|
||||
SESSION_ID="$(tmux display-message -p '#{session_id}')"
|
||||
|
||||
# session 级环境变量
|
||||
tmux setenv -t "$SESSION_ID" OPENCODE_MODEL "$MODEL"
|
||||
|
||||
# pane 级选项(.bashrc 会在新 shell 启动时读取)
|
||||
tmux set-option -p @opencode_model "$MODEL"
|
||||
fi
|
||||
|
||||
# ── OpenCode 自更新检查(每日一次)──
|
||||
LAST_UPDATE_FILE="$HOME/.cache/opencode_update_last_run"
|
||||
TODAY=$(date +%Y-%m-%d)
|
||||
mkdir -p "$(dirname "$LAST_UPDATE_FILE")"
|
||||
|
||||
if [ ! -f "$LAST_UPDATE_FILE" ] || [ "$(cat "$LAST_UPDATE_FILE" 2>/dev/null)" != "$TODAY" ]; then
|
||||
opencode upgrade 2>/dev/null || true
|
||||
echo "$TODAY" > "$LAST_UPDATE_FILE"
|
||||
fi
|
||||
|
||||
# ── 构建启动参数 ──
|
||||
OC_ARGS=(-m "$MODEL")
|
||||
|
||||
echo "启动 OpenCode (模型: $MODEL)"
|
||||
opencode "${OC_ARGS[@]}"
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "{{your_api_key}}",
|
||||
"ANTHROPIC_BASE_URL": "https://open.bigmodel.cn/api/anthropic",
|
||||
"API_TIMEOUT_MS": "3000000",
|
||||
"CLAUDE_CODE_AUTO_COMPACT_WINDOW": "200000",
|
||||
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
|
||||
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1",
|
||||
"CLAUDE_AUTOCOMPACT_PCT_OVERRIDE": "75",
|
||||
"CLAUDE_CODE_NO_FLICKER": "1"
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"code-simplifier@claude-plugins-official": true,
|
||||
"code-review@claude-plugins-official": true,
|
||||
"hookify@claude-plugins-official": true,
|
||||
"ralph-loop@claude-plugins-official": true,
|
||||
"skill-creator@claude-plugins-official": true,
|
||||
"typescript-lsp@claude-plugins-official": true,
|
||||
"pyright-lsp@claude-plugins-official": true,
|
||||
"superpowers@claude-plugins-official": true,
|
||||
"glm-plan-usage@zai-coding-plugins": true
|
||||
},
|
||||
"language": "Chinese",
|
||||
"skipDangerousModePermissionPrompt": true
|
||||
}
|
||||
Reference in New Issue
Block a user