#!/bin/bash # Claude Code 启动脚本 # 支持多 Provider (通过 .env 配置,自动解析 ${PROVIDER}_BASE_URL / ${PROVIDER}_AUTH_TOKEN) # # 每天首次启动时自动执行 claude upgrade # 始终通过 tmux 启动 claude,每次启动创建独立 session(以当前目录名命名) # 同目录可多开,session 名末尾自增数字(dit-1 → dit-2 → dit-3) # pane 标题与 CC --name 使用 session_model 组合(如 dit-1_glm-5.2),window 名使用模型名 # # tmux 配置参考: https://gitea.szis.com.cn/isos/team/src/branch/trunk/team/tmux.md # ── 路径配置 ────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="${SCRIPT_DIR}/.env" # ── 加载 .env 文件 ────────────────────────────────────── if [ -f "$ENV_FILE" ]; then while IFS= read -r line || [ -n "$line" ]; do # 跳过注释和空行 [[ "$line" =~ ^[[:space:]]*# ]] && continue [[ -z "${line// /}" ]] && continue # 解析 key=value key="${line%%=*}" value="${line#*=}" # 去除引号 [[ "$value" =~ ^\"(.*)\"$ ]] && value="${BASH_REMATCH[1]}" [[ "$value" =~ ^\'(.*)\'$ ]] && value="${BASH_REMATCH[1]}" # 展开 value 中的环境变量引用(如 $HOME → /home/arno) eval "value=\"$value\"" # 仅导出合法变量名 if [[ "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then export "$key=$value" fi done < "$ENV_FILE" fi # ── 参数解析 ────────────────────────────────────────────── SESSION_NAME="" MODEL_KEY="" PROVIDER_OVERRIDE="" ACTION="launch" # launch | list | kill | send KILL_TARGET="" SEND_TARGET="" SEND_MESSAGE="" while [ $# -gt 0 ]; do case "$1" in --name|-n) [ $# -ge 2 ] || { echo "错误: --name 需要参数" >&2; exit 1; } SESSION_NAME="$2"; shift 2 ;; --provider|-p) [ $# -ge 2 ] || { echo "错误: --provider 需要参数" >&2; exit 1; } PROVIDER_OVERRIDE="$2"; shift 2 ;; --list|-l) ACTION="list"; shift ;; --kill|-k) [ $# -ge 2 ] || { echo "错误: --kill 需要 session 名称参数" >&2; exit 1; } ACTION="kill"; KILL_TARGET="$2"; shift 2 ;; --send|-s) [ $# -ge 3 ] || { echo "错误: --send 需要 <消息>" >&2; exit 1; } ACTION="send"; SEND_TARGET="$2"; SEND_MESSAGE="$3"; shift 3 ;; -h|--help) echo "用法: runcc.sh [选项] [模型]" echo "" echo "操作模式:" echo " (默认) 启动 Claude Code 会话" echo " --list, -l 列出所有 CC tmux 会话(含 provider/模型信息)" echo " --kill, -k <名称> 终止指定 CC 会话" echo " --send, -s <会话> <消息> 向已有 CC 会话发送提示词" echo "" echo "启动选项:" echo " --name, -n <名称> 会话显示名称" echo " --provider, -p <名称> API 提供商 (由 .env 配置,如 glm / deepseek)" echo "" echo "GLM 模型: 45|45a|46|47|5|5t|51|52" echo "Qwen 模型: qw37|qw36f" echo "DeepSeek 模型: ds|dsf" echo "其他: 直接传入模型名称" echo "" echo "Provider 变量规则: CC_PROVIDER=_BASE_URL + _AUTH_TOKEN" echo " 添加新 Provider 只需在 .env 中按此格式添加变量即可" echo "" echo "快捷方式 (通过 ~/.bashrc 别名):" echo " cc [模型] 默认 glm provider" echo " ccds [模型] 默认 deepseek provider" echo "" echo "示例:" echo " runcc.sh # 默认 Provider + 默认模型" echo " runcc.sh 47 # 默认 Provider + glm-4.7" echo " runcc.sh --provider qwen # Qwen + 默认模型" echo " runcc.sh -p qwen qw37 # Qwen + qwen3.7-max" echo " runcc.sh -l # 列出所有 CC 会话" echo " runcc.sh -k glm-52 # 终止 glm-52 会话" echo " runcc.sh -s glm-52 '你好' # 向 glm-52 发送消息" exit 0 ;; *) MODEL_KEY="$1"; shift ;; esac done # ── 操作: --list ────────────────────────────────────────── if [ "$ACTION" = "list" ]; then echo "CC tmux 会话列表:" echo "──────────────────────────────────────────────────────" CC_COUNT=0 for s in $(tmux list-sessions -F '#{session_name}' 2>/dev/null); do if tmux show-options -t "$s" @cc_provider 2>/dev/null | grep -q .; then CC_COUNT=$((CC_COUNT + 1)) created=$(tmux display -t "$s" -p '#{session_created}' 2>/dev/null) attached=$(tmux display -t "$s" -p '#{?session_attached,●,○}' 2>/dev/null) provider=$(tmux show-options -t "$s" -v @cc_provider 2>/dev/null) model=$(tmux show-options -t "$s" -v @cc_model 2>/dev/null) printf " %s %s %s @cc_provider=%s @cc_model=%s\n" "$attached" "$s" "$created" "$provider" "$model" fi done if [ "$CC_COUNT" -eq 0 ]; then echo " (无 CC 会话)" fi echo "──────────────────────────────────────────────────────" echo "●=已附加 ○=后台运行" exit 0 fi # ── 操作: --kill ────────────────────────────────────────── if [ "$ACTION" = "kill" ]; then if ! tmux has-session -t "$KILL_TARGET" 2>/dev/null; then echo "错误: 会话 '$KILL_TARGET' 不存在" >&2 echo "可用 CC 会话:" CC_LISTED=0 for s in $(tmux list-sessions -F '#{session_name}' 2>/dev/null); do if tmux show-options -t "$s" @cc_provider 2>/dev/null | grep -q .; then echo " $s" CC_LISTED=1 fi done [ "$CC_LISTED" -eq 0 ] && echo " (无)" exit 1 fi tmux kill-session -t "$KILL_TARGET" echo "已终止会话: $KILL_TARGET" exit 0 fi # ── 操作: --send ────────────────────────────────────────── if [ "$ACTION" = "send" ]; then if ! tmux has-session -t "$SEND_TARGET" 2>/dev/null; then echo "错误: 会话 '$SEND_TARGET' 不存在" >&2 exit 1 fi # 找第一个 pane(通常是 claude 主进程) PANE_ID="$(tmux list-panes -t "$SEND_TARGET" -F '#{pane_id}' | head -1)" tmux send-keys -t "$PANE_ID" -l "$SEND_MESSAGE" tmux send-keys -t "$PANE_ID" Enter echo "已向 $SEND_TARGET ($PANE_ID) 发送消息" exit 0 fi # ── Provider 动态解析 ───────────────────────────────────── # 优先级:命令行 --provider > .env CC_PROVIDER > 默认 glm CC_PROVIDER="${PROVIDER_OVERRIDE:-${CC_PROVIDER:-glm}}" upper_provider="$(echo "$CC_PROVIDER" | tr '[:lower:]' '[:upper:]')" base_url_var="${upper_provider}_BASE_URL" token_var="${upper_provider}_AUTH_TOKEN" default_model_var="${upper_provider}_DEFAULT_MODEL" context_window_var="${upper_provider}_CONTEXT_WINDOW" compact_pct_var="${upper_provider}_COMPACT_PCT" disable_compact_var="${upper_provider}_DISABLE_COMPACT" max_ctx_var="${upper_provider}_MAX_CONTEXT_TOKENS" ANTHROPIC_BASE_URL="${!base_url_var}" ANTHROPIC_AUTH_TOKEN="${!token_var}" DEFAULT_MODEL="${!default_model_var:-}" CONTEXT_WINDOW="${!context_window_var:-200000}" COMPACT_PCT="${!compact_pct_var:-75}" DISABLE_COMPACT="${!disable_compact_var:-}" MAX_CONTEXT_TOKENS="${!max_ctx_var:-}" if [ -z "$ANTHROPIC_BASE_URL" ] || [ -z "$ANTHROPIC_AUTH_TOKEN" ]; then echo "错误: Provider '$CC_PROVIDER' 未配置 BASE_URL 或 AUTH_TOKEN" >&2 echo "请在 ${ENV_FILE} 中添加: ${upper_provider}_BASE_URL, ${upper_provider}_AUTH_TOKEN" >&2 exit 1 fi export ANTHROPIC_BASE_URL ANTHROPIC_AUTH_TOKEN export CC_CONTEXT_WINDOW="$CONTEXT_WINDOW" # DISABLE_COMPACT 模式: 禁用自动压缩,覆盖上下文窗口(适用于 API 报告值 < 实际值的模型) if [ "$DISABLE_COMPACT" = "1" ]; then export DISABLE_COMPACT=1 export CLAUDE_CODE_MAX_CONTEXT_TOKENS="${MAX_CONTEXT_TOKENS:-$CONTEXT_WINDOW}" else export CLAUDE_AUTOCOMPACT_PCT_OVERRIDE="$COMPACT_PCT" fi # ── 更新 settings.json ────────────────────────────────── # 1) 删除凭据、模型和窗口变量(由环境变量接管) # 2) 注入 statusLine / UserPromptSubmit hook(不存在则写入绝对路径) # SETTINGS_FILE / STATUS_LINE_SCRIPT / USER_PROMPT_SUBMIT_SCRIPT 路径由 .env 配置 SETTINGS_FILE="${SETTINGS_FILE:-$HOME/.claude/settings.json}" # 项目根目录:优先用 .env 的 SKILLS_DIR(语义清晰、跨机器可迁移);未配置则从脚本位置反推兜底 PROJECT_DIR="${SKILLS_DIR:-$(dirname "$(dirname "$SCRIPT_DIR")")}" STATUS_LINE_SCRIPT="${STATUS_LINE_SCRIPT:-.claude/tools/status_line.sh}" USER_PROMPT_SUBMIT_SCRIPT="${USER_PROMPT_SUBMIT_SCRIPT:-.claude/tools/user-prompt-submit.sh}" # 相对路径 → 基于项目根目录的绝对路径 if [[ "$STATUS_LINE_SCRIPT" != /* ]]; then STATUS_LINE_SCRIPT="${PROJECT_DIR}/${STATUS_LINE_SCRIPT#./}" fi if [[ "$USER_PROMPT_SUBMIT_SCRIPT" != /* ]]; then USER_PROMPT_SUBMIT_SCRIPT="${PROJECT_DIR}/${USER_PROMPT_SUBMIT_SCRIPT#./}" fi if command -v jq &>/dev/null && [ -f "$SETTINGS_FILE" ]; then # 清理残留的环境变量配置 jq 'del(.env.ANTHROPIC_BASE_URL, .env.ANTHROPIC_AUTH_TOKEN, .env.CLAUDE_CODE_AUTO_COMPACT_WINDOW, .env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE, .env.DISABLE_COMPACT, .env.CLAUDE_CODE_MAX_CONTEXT_TOKENS, .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" # 确保 statusLine 已配置且脚本可执行(键不存在或路径失效则写入/纠正) SL_CUR="$(jq -r '.statusLine.command // empty' "$SETTINGS_FILE" 2>/dev/null)" if [ -z "$SL_CUR" ] || [ ! -x "${SL_CUR%% *}" ]; then jq --arg cmd "$STATUS_LINE_SCRIPT" \ '. + {"statusLine": {"type": "command", "command": $cmd}}' \ "$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" \ && mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE" fi # 确保 UserPromptSubmit hook 已配置且脚本可执行(键不存在或路径失效则写入/纠正) UPS_CUR="$(jq -r '.hooks.UserPromptSubmit[0].hooks[0].command // empty' "$SETTINGS_FILE" 2>/dev/null)" if [ -z "$UPS_CUR" ] || [ ! -x "${UPS_CUR%% *}" ]; then jq --arg cmd "$USER_PROMPT_SUBMIT_SCRIPT" \ '.hooks += {"UserPromptSubmit": [{"hooks": [{"type": "command", "command": $cmd}]}]}' \ "$SETTINGS_FILE" > "${SETTINGS_FILE}.tmp" \ && mv "${SETTINGS_FILE}.tmp" "$SETTINGS_FILE" fi fi # ── 模型解析 ────────────────────────────────────────────── MODEL_KEY="${MODEL_KEY:-$DEFAULT_MODEL}" case "$MODEL_KEY" in # GLM 系列 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" ;; 52) MODEL="glm-5.2" ;; # Qwen 系列 qw37) MODEL="qwen3.7-max" ;; qw36f) MODEL="qwen3.6-flash" ;; qwim) MODEL="qwen-image-max" ;; # DeepSeek 系列 ds) MODEL="deepseek-v4-pro[1m]" ;; dsf) MODEL="deepseek-v4-flash" ;; # 其他: 直接作为模型名称 *) MODEL="$MODEL_KEY" ;; esac # ── 模型环境变量(三个层级统一为同一模型)──────────────── export ANTHROPIC_MODEL="$MODEL" export ANTHROPIC_DEFAULT_HAIKU_MODEL="$MODEL" export ANTHROPIC_DEFAULT_SONNET_MODEL="$MODEL" export ANTHROPIC_DEFAULT_OPUS_MODEL="$MODEL" export CLAUDE_CODE_SUBAGENT_MODEL="$MODEL" export CLAUDE_CODE_NO_FLICKER=1 # ── 会话命名规则:目录名-序号(如 dit-1、szis-2)─────────── DIR_NAME=$(basename "$PWD") BASE_TITLE="${DIR_NAME}" if [ -z "$SESSION_NAME" ]; then SESSION_NAME="${BASE_TITLE}-1" fi # ── 构建 claude 启动参数 ───────────────────────────────── CC_ARGS=(--ide --dangerously-skip-permissions --allow-dangerously-skip-permissions --effort max) # 公共 skills 仓库(由 .env 的 SKILLS_DIR 配置,便于跨电脑迁移) if [ -n "${SKILLS_DIR:-}" ]; then CC_ARGS+=(--add-dir "$SKILLS_DIR") fi # ── 每日自动更新 ──────────────────────────────────────── UPGRADE_MARKER="${HOME}/.claude/.last-upgrade-date" TODAY="$(date +%Y-%m-%d)" if [ ! -f "$UPGRADE_MARKER" ] || [ "$(cat "$UPGRADE_MARKER" 2>/dev/null)" != "$TODAY" ]; then echo "每日首次启动,检查 Claude Code 更新..." if claude upgrade 2>&1; then mkdir -p "$(dirname "$UPGRADE_MARKER")" echo "$TODAY" > "$UPGRADE_MARKER" else echo "警告: claude upgrade 失败,跳过更新继续启动" fi fi # ── 辅助函数:自动编号(列表中出现同名则递增)───────────── # 用法: _auto_number "名字列表(换行分隔)" "基础名" → 输出下一个可用名 _auto_number() { local list="$1" local base="$2" local name="$base" local n=1 while echo "$list" | grep -qxF "$name"; do n=$((n + 1)) name="${base}-${n}" done echo "$name" } # ════════════════════════════════════════════════════════════ # ── 启动 Claude Code(始终通过 tmux)────────────────────── # ════════════════════════════════════════════════════════════ ORIG_PWD="$PWD" WINDOW_NAME="$MODEL" # ── 构建 -e 参数数组 ──────────────────────────────────── E_FLAGS=() E_FLAGS+=(-e "ANTHROPIC_BASE_URL=$ANTHROPIC_BASE_URL") E_FLAGS+=(-e "ANTHROPIC_AUTH_TOKEN=$ANTHROPIC_AUTH_TOKEN") E_FLAGS+=(-e "ANTHROPIC_MODEL=$MODEL") E_FLAGS+=(-e "ANTHROPIC_DEFAULT_OPUS_MODEL=$MODEL") E_FLAGS+=(-e "ANTHROPIC_DEFAULT_HAIKU_MODEL=$MODEL") E_FLAGS+=(-e "ANTHROPIC_DEFAULT_SONNET_MODEL=$MODEL") E_FLAGS+=(-e "CLAUDE_CODE_SUBAGENT_MODEL=$MODEL") E_FLAGS+=(-e "CC_CONTEXT_WINDOW=$CONTEXT_WINDOW") E_FLAGS+=(-e "CLAUDE_CODE_NO_FLICKER=1") if [ "$DISABLE_COMPACT" = "1" ]; then E_FLAGS+=(-e "DISABLE_COMPACT=1") E_FLAGS+=(-e "CLAUDE_CODE_MAX_CONTEXT_TOKENS=${MAX_CONTEXT_TOKENS:-$CONTEXT_WINDOW}") else E_FLAGS+=(-e "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=$COMPACT_PCT") fi if [ -n "${TMUX:-}" ]; then # ── 已在 tmux 中:pane 标题 = session_model → 设置 env → 直接启动 TMUX_TITLE="${SESSION_NAME}_${MODEL}" CC_ARGS+=(--name "$TMUX_TITLE") echo "启动 Claude Code (provider: $CC_PROVIDER, 模型: $MODEL, 会话: $SESSION_NAME)" tmux select-pane -T "$TMUX_TITLE" 2>/dev/null || true # 传播环境变量到 tmux session(供子 pane/window 继承) SID="$(tmux display-message -p '#{session_id}')" tmux setenv -t "$SID" ANTHROPIC_BASE_URL "$ANTHROPIC_BASE_URL" tmux setenv -t "$SID" ANTHROPIC_AUTH_TOKEN "$ANTHROPIC_AUTH_TOKEN" tmux setenv -t "$SID" ANTHROPIC_MODEL "$MODEL" tmux setenv -t "$SID" ANTHROPIC_DEFAULT_OPUS_MODEL "$MODEL" tmux setenv -t "$SID" ANTHROPIC_DEFAULT_HAIKU_MODEL "$MODEL" tmux setenv -t "$SID" ANTHROPIC_DEFAULT_SONNET_MODEL "$MODEL" tmux setenv -t "$SID" CLAUDE_CODE_SUBAGENT_MODEL "$MODEL" tmux setenv -t "$SID" CC_CONTEXT_WINDOW "$CONTEXT_WINDOW" tmux setenv -t "$SID" CLAUDE_CODE_NO_FLICKER 1 if [ "$DISABLE_COMPACT" = "1" ]; then tmux setenv -t "$SID" DISABLE_COMPACT 1 tmux setenv -t "$SID" CLAUDE_CODE_MAX_CONTEXT_TOKENS "${MAX_CONTEXT_TOKENS:-$CONTEXT_WINDOW}" tmux setenv -u -t "$SID" CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 2>/dev/null || true else tmux setenv -t "$SID" CLAUDE_AUTOCOMPACT_PCT_OVERRIDE "$COMPACT_PCT" tmux setenv -u -t "$SID" DISABLE_COMPACT 2>/dev/null || true tmux setenv -u -t "$SID" CLAUDE_CODE_MAX_CONTEXT_TOKENS 2>/dev/null || true fi # 设置 pane 级属性(供 status_line 等读取) tmux set-option -p @anthropic_model "$MODEL" 2>/dev/null || true # 环境变量已在当前 shell 中 export,直接 exec cd "$ORIG_PWD" exec claude "${CC_ARGS[@]}" else # ── 不在 tmux 中:以目录名-序号创建独立 session(如 dit-1、dit-2) n=1 while tmux has-session -t "${BASE_TITLE}-${n}" 2>/dev/null; do n=$((n + 1)) done TMUX_SESSION="${BASE_TITLE}-${n}" SESSION_NAME="$TMUX_SESSION" TMUX_TITLE="${SESSION_NAME}_${MODEL}" CC_ARGS+=(--name "$TMUX_TITLE") echo "启动 Claude Code (provider: $CC_PROVIDER, 模型: $MODEL, tmux session: $TMUX_SESSION)" # 构建 claude 启动命令 CC_CMD="claude" for arg in "${CC_ARGS[@]}"; do CC_CMD+=" $arg" done BASH_WRAPPER="if ! command -v claude &>/dev/null; then echo '错误: claude 命令未找到' echo -n '按 Enter 退出...'; read exit 1 fi exec ${CC_CMD}" # 创建独立 tmux session(session 名 = MODEL 名) tmux new-session -d -s "$TMUX_SESSION" -n "$WINDOW_NAME" -c "$ORIG_PWD" \ "${E_FLAGS[@]}" bash -c "$BASH_WRAPPER" # 设置 pane 标题 tmux select-pane -t "$TMUX_SESSION:$WINDOW_NAME" -T "$TMUX_TITLE" 2>/dev/null || true # ── tmux 会话级默认配置 ────────────────────────────── # 参考: https://gitea.szis.com.cn/isos/team/src/branch/trunk/team/tmux.md _apply_tmux_defaults() { local sess="$1" # 基础选项 tmux set-option -t "$sess" mouse on 2>/dev/null || true tmux set-option -t "$sess" base-index 1 2>/dev/null || true # tmux set-option -t "$sess" default-terminal "screen-256color" 2>/dev/null || true tmux set-option -t "$sess" escape-time 0 2>/dev/null || true tmux set-option -t "$sess" history-limit 50000 2>/dev/null || true tmux set-option -t "$sess" set-titles on 2>/dev/null || true tmux set-option -t "$sess" focus-events on 2>/dev/null || true tmux set-option -t "$sess" set-clipboard on 2>/dev/null || true tmux set-option -t "$sess" renumber-windows on 2>/dev/null || true # 状态栏 tmux set-option -t "$sess" status-left "#S |" 2>/dev/null || true tmux set-option -t "$sess" status-left-length 20 2>/dev/null || true tmux set-option -t "$sess" status-right "%Y-%m-%d %H:%M" 2>/dev/null || true tmux set-option -t "$sess" status-right-length 20 2>/dev/null || true # 窗口选项 tmux set-window-option -t "$sess" pane-base-index 1 2>/dev/null || true tmux set-window-option -t "$sess" mode-keys vi 2>/dev/null || true tmux set-window-option -t "$sess" aggressive-resize on 2>/dev/null || true # 存储 provider/model 元信息(供 --list 查询) tmux set-option -t "$sess" @cc_provider "$CC_PROVIDER" 2>/dev/null || true tmux set-option -t "$sess" @cc_model "$MODEL" 2>/dev/null || true } _apply_tmux_defaults "$TMUX_SESSION" # 附加到会话(前台运行) tmux attach-session -t "$TMUX_SESSION" fi