#!/bin/bash # 设置错误处理 set -e # 定义日志文件路径 LOG_FILE="/tmp/cursor_linux_id_modifier.log" # 初始化日志文件 initialize_log() { echo "========== Cursor ID 修改工具日志开始 $(date) ==========" > "$LOG_FILE" chmod 644 "$LOG_FILE" } # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # 显式禁用时,关闭 TTY UI(resize/clear/Logo),避免部分环境乱码/花屏 if [ -n "${CURSOR_NO_TTY_UI:-}" ]; then CURSOR_NO_TTY_UI=1 fi # UI/颜色开关:遵循 NO_COLOR 标准,并支持 CURSOR_NO_TTY_UI(禁用花哨 TTY UI) if [ -n "${NO_COLOR:-}" ] || [ -n "${CURSOR_NO_COLOR:-}" ] || [ -n "${CURSOR_NO_TTY_UI:-}" ]; then RED='' GREEN='' YELLOW='' BLUE='' NC='' fi # 启动时尝试调整终端窗口大小为 120x40(列x行);不支持/失败时静默忽略,避免影响脚本主流程 try_resize_terminal_window() { local target_cols=120 local target_rows=40 # 可通过 CURSOR_NO_TTY_UI 显式禁用所有终端控制输出(避免部分环境乱码/花屏) if [ -n "${CURSOR_NO_TTY_UI:-}" ]; then return 0 fi # 仅在交互终端中尝试,避免输出被重定向时出现乱码 if [ ! -t 1 ]; then return 0 fi case "${TERM:-}" in ""|dumb) return 0 ;; esac # 终端类型检测:仅对常见 xterm 体系终端尝试窗口调整(GNOME Terminal/Konsole/xterm/Terminator 等通常为 xterm*) case "${TERM:-}" in xterm*|screen*|tmux*|rxvt*|alacritty*|kitty*|foot*|wezterm*) ;; *) return 0 ;; esac # 优先通过 xterm 窗口控制序列调整;在 tmux/screen 下需要 passthrough 包装 if [ -n "${TMUX:-}" ]; then printf '\033Ptmux;\033\033[8;%d;%dt\033\\' "$target_rows" "$target_cols" 2>/dev/null || true elif [ -n "${STY:-}" ]; then printf '\033P\033[8;%d;%dt\033\\' "$target_rows" "$target_cols" 2>/dev/null || true else printf '\033[8;%d;%dt' "$target_rows" "$target_cols" 2>/dev/null || true fi return 0 } # 日志函数 - 同时输出到终端和日志文件 log_info() { echo -e "${GREEN}[INFO]${NC} $1" echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE" } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" echo "[WARN] $(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE" } log_error() { echo -e "${RED}[ERROR]${NC} $1" echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE" } log_debug() { echo -e "${BLUE}[DEBUG]${NC} $1" echo "[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE" } # 记录命令输出到日志文件 log_cmd_output() { local cmd="$1" local msg="$2" echo "[CMD] $(date '+%Y-%m-%d %H:%M:%S') 执行命令: $cmd" >> "$LOG_FILE" echo "[CMD] $msg:" >> "$LOG_FILE" eval "$cmd" 2>&1 | tee -a "$LOG_FILE" echo "" >> "$LOG_FILE" } # sed -i 兼容封装:优先原地编辑;不支持/失败时回退到临时文件替换,提升跨发行版兼容性 sed_inplace() { local expr="$1" local file="$2" # GNU sed / BusyBox sed:通常支持 sed -i if sed -i "$expr" "$file" 2>/dev/null; then return 0 fi # BSD sed:需要提供 -i '' 形式(少数环境可能出现) if sed -i '' "$expr" "$file" 2>/dev/null; then return 0 fi # 最后兜底:临时文件替换(避免不同 sed 的 -i 语义差异) local temp_file temp_file=$(mktemp) || return 1 if sed "$expr" "$file" > "$temp_file"; then cat "$temp_file" > "$file" rm -f "$temp_file" return 0 fi rm -f "$temp_file" return 1 } # 路径解析兼容:优先 realpath;缺失时回退到 readlink -f / python3 / cd+pwd(避免命令缺失触发 set -e) resolve_path() { local target="$1" if command -v realpath >/dev/null 2>&1; then realpath "$target" 2>/dev/null && return 0 fi if command -v readlink >/dev/null 2>&1; then readlink -f "$target" 2>/dev/null && return 0 fi if command -v python3 >/dev/null 2>&1; then python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$target" 2>/dev/null && return 0 fi # 最后兜底:不解析符号链接,仅尽量返回绝对路径 if [ -d "$target" ]; then (cd "$target" 2>/dev/null && pwd -P) && return 0 fi local dir dir=$(dirname "$target") (cd "$dir" 2>/dev/null && printf "%s/%s\n" "$(pwd -P)" "$(basename "$target")") && return 0 echo "$target" return 0 } # 获取当前用户 get_current_user() { # sudo 场景:优先以 SUDO_USER 作为目标用户(Cursor 通常运行在该用户下) if [ "$EUID" -eq 0 ] && [ -n "${SUDO_USER:-}" ]; then echo "$SUDO_USER" return 0 fi # 普通/直跑 root 场景:使用当前有效用户 if command -v id >/dev/null 2>&1; then id -un 2>/dev/null && return 0 fi echo "${USER:-}" } # 获取指定用户的 Home 目录(兼容 sudo/root/容器等场景) get_user_home_dir() { local user="$1" local home="" if command -v getent >/dev/null 2>&1; then home=$(getent passwd "$user" 2>/dev/null | awk -F: '{print $6}' | head -n 1) fi if [ -z "$home" ] && [ -f /etc/passwd ]; then home=$(awk -F: -v u="$user" '$1==u {print $6; exit}' /etc/passwd 2>/dev/null) fi if [ -z "$home" ]; then home=$(eval echo "~$user" 2>/dev/null) fi # 兜底:无法解析时使用当前环境 HOME if [ -z "$home" ] || [[ "$home" == "~"* ]]; then home="${HOME:-}" fi echo "$home" } # 获取指定用户的主组(chown 需要 user:group;不同发行版 id 参数/输出可能存在差异) get_user_primary_group() { local user="$1" local group="" local gid="" # 优先:直接获取主组名(最简洁) if command -v id >/dev/null 2>&1; then group=$(id -gn "$user" 2>/dev/null | tr -d '\r\n') || true if [ -n "$group" ]; then echo "$group" return 0 fi # 回退:先取 gid,再映射为组名(映射失败则直接返回 gid,chown 同样可用) gid=$(id -g "$user" 2>/dev/null | tr -d '\r\n') || true fi if [ -n "$gid" ]; then if command -v getent >/dev/null 2>&1; then group=$(getent group "$gid" 2>/dev/null | awk -F: '{print $1}' | head -n 1) || true fi if [ -z "$group" ] && [ -f /etc/group ]; then group=$(awk -F: -v g="$gid" '$3==g {print $1; exit}' /etc/group 2>/dev/null) || true fi if [ -n "$group" ]; then echo "$group" return 0 fi echo "$gid" return 0 fi # 最后兜底:返回用户本身(少数系统允许 user:user) echo "$user" return 0 } CURRENT_USER=$(get_current_user) if [ -z "$CURRENT_USER" ]; then log_error "无法获取用户名" exit 1 fi # 🎯 统一“目标用户/目标 Home”:后续所有 Cursor 用户数据路径均基于该 Home TARGET_HOME=$(get_user_home_dir "$CURRENT_USER") if [ -z "$TARGET_HOME" ]; then log_error "无法解析目标用户 Home 目录: $CURRENT_USER" exit 1 fi log_info "目标用户: $CURRENT_USER" log_info "目标用户 Home: $TARGET_HOME" # 🎯 统一“目标用户主组”:chown 时不再依赖 id -g -n 的兼容性 CURRENT_GROUP=$(get_user_primary_group "$CURRENT_USER") if [ -z "$CURRENT_GROUP" ]; then CURRENT_GROUP="$CURRENT_USER" log_warn "无法解析目标用户主组,已回退为: $CURRENT_GROUP(后续 chown 可能失败)" else log_info "目标用户主组: $CURRENT_GROUP" fi # 定义Linux下的Cursor路径 CURSOR_CONFIG_DIR="$TARGET_HOME/.config/Cursor" STORAGE_FILE="$CURSOR_CONFIG_DIR/User/globalStorage/storage.json" BACKUP_DIR="$CURSOR_CONFIG_DIR/User/globalStorage/backups" # 共享ID(用于配置与JS注入保持一致) CURSOR_ID_MACHINE_ID="" CURSOR_ID_MACHINE_GUID="" CURSOR_ID_MAC_MACHINE_ID="" CURSOR_ID_DEVICE_ID="" CURSOR_ID_SQM_ID="" CURSOR_ID_FIRST_SESSION_DATE="" CURSOR_ID_SESSION_ID="" CURSOR_ID_MAC_ADDRESS="00:11:22:33:44:55" # --- 新增:安装相关变量 --- APPIMAGE_SEARCH_DIR="/opt/CursorInstall" # AppImage 搜索目录,可按需修改 APPIMAGE_PATTERN="Cursor-*.AppImage" # AppImage 文件名模式 INSTALL_DIR="/opt/Cursor" # Cursor 最终安装目录 ICON_PATH="/usr/share/icons/cursor.png" DESKTOP_FILE="/usr/share/applications/cursor-cursor.desktop" # --- 结束:安装相关变量 --- # 可能的Cursor二进制路径 - 添加了标准安装路径 CURSOR_BIN_PATHS=( "/usr/bin/cursor" "/usr/local/bin/cursor" "$INSTALL_DIR/cursor" # 添加标准安装路径 "$TARGET_HOME/.local/bin/cursor" "/snap/bin/cursor" ) # 找到Cursor安装路径 find_cursor_path() { log_info "查找Cursor安装路径..." for path in "${CURSOR_BIN_PATHS[@]}"; do if [ -f "$path" ] && [ -x "$path" ]; then # 确保文件存在且可执行 log_info "找到Cursor安装路径: $path" CURSOR_PATH="$path" return 0 fi done # 尝试通过which命令定位 if command -v cursor &> /dev/null; then # 兼容修复:部分发行版没有 which;command -v 已可直接返回路径 CURSOR_PATH=$(command -v cursor) log_info "通过 command -v 找到 Cursor: $CURSOR_PATH" return 0 fi # 尝试查找可能的安装路径 (限制搜索范围和类型) # 兼容修复:find 的 -executable 在 BusyBox 等环境可能不可用,且 find 报错返回非0会触发 set -e;这里统一兜底处理 local cursor_paths="" # 优先:使用 -executable(若受支持) cursor_paths=$(find /usr /opt "$TARGET_HOME/.local" -type f -name "cursor" -executable 2>/dev/null || true) # 回退:不依赖 -executable,改用 shell 过滤可执行 if [ -z "$cursor_paths" ]; then cursor_paths=$(find /usr /opt "$TARGET_HOME/.local" -type f -name "cursor" 2>/dev/null || true) cursor_paths=$(echo "$cursor_paths" | while IFS= read -r p; do [ -n "$p" ] && [ -x "$p" ] && echo "$p"; done) fi # 额外兜底:标准安装路径优先 if [ -x "$INSTALL_DIR/cursor" ]; then cursor_paths="$INSTALL_DIR/cursor"$'\n'"$cursor_paths" fi if [ -n "$cursor_paths" ]; then # 优先选择标准安装路径 local standard_path=$(echo "$cursor_paths" | grep "$INSTALL_DIR/cursor" | head -1) if [ -n "$standard_path" ]; then CURSOR_PATH="$standard_path" else CURSOR_PATH=$(echo "$cursor_paths" | head -1) fi log_info "通过查找找到Cursor: $CURSOR_PATH" return 0 fi log_warn "未找到Cursor可执行文件" return 1 } # 查找并定位Cursor资源文件目录 find_cursor_resources() { log_info "查找Cursor资源目录..." # 可能的资源目录路径 - 添加了标准安装目录 local resource_paths=( "$INSTALL_DIR" # 添加标准安装路径 "/usr/lib/cursor" "/usr/share/cursor" "$TARGET_HOME/.local/share/cursor" ) for path in "${resource_paths[@]}"; do if [ -d "$path/resources" ]; then # 检查是否存在 resources 子目录 log_info "找到Cursor资源目录: $path" CURSOR_RESOURCES="$path" return 0 fi if [ -d "$path/app" ]; then # 有些版本可能直接是 app 目录 log_info "找到Cursor资源目录 (app): $path" CURSOR_RESOURCES="$path" return 0 fi done # 如果有CURSOR_PATH,尝试从它推断 if [ -n "$CURSOR_PATH" ]; then local base_dir=$(dirname "$CURSOR_PATH") # 检查常见的相对路径 if [ -d "$base_dir/resources" ]; then CURSOR_RESOURCES="$base_dir" log_info "通过二进制路径找到资源目录: $CURSOR_RESOURCES" return 0 elif [ -d "$base_dir/../resources" ]; then # 例如在 bin 目录内 CURSOR_RESOURCES=$(resolve_path "$base_dir/..") log_info "通过二进制路径找到资源目录: $CURSOR_RESOURCES" return 0 elif [ -d "$base_dir/../lib/cursor/resources" ]; then # 另一种常见结构 CURSOR_RESOURCES=$(resolve_path "$base_dir/../lib/cursor") log_info "通过二进制路径找到资源目录: $CURSOR_RESOURCES" return 0 fi fi log_warn "未找到Cursor资源目录" return 1 } # 检查权限 check_permissions() { if [ "$EUID" -ne 0 ]; then log_error "请使用 sudo 运行此脚本 (安装和修改系统文件需要权限)" echo "示例: sudo $0" exit 1 fi } # --- 新增/重构:从本地 AppImage 安装 Cursor --- install_cursor_appimage() { log_info "开始尝试从本地 AppImage 安装 Cursor..." local found_appimage_path="" # 确保搜索目录存在 mkdir -p "$APPIMAGE_SEARCH_DIR" # 查找 AppImage 文件 find_appimage() { # 兼容修复:find 参数在不同实现中可能有差异,且 find 非0 会触发 set -e;这里统一兜底为成功返回 found_appimage_path=$(find "$APPIMAGE_SEARCH_DIR" -maxdepth 1 -name "$APPIMAGE_PATTERN" -print -quit 2>/dev/null || true) if [ -z "$found_appimage_path" ]; then return 1 else return 0 fi } if ! find_appimage; then log_warn "在 '$APPIMAGE_SEARCH_DIR' 目录下未找到 '$APPIMAGE_PATTERN' 文件。" # --- 新增:添加文件名格式提醒 --- log_info "请确保 AppImage 文件名格式类似: Cursor-版本号-架构.AppImage (例如: Cursor-1.0.6-aarch64.AppImage 或 Cursor-x.y.z-x86_64.AppImage)" # --- 结束:添加文件名格式提醒 --- # 等待用户放置文件 read -p $"请将 Cursor AppImage 文件放入 '$APPIMAGE_SEARCH_DIR' 目录,然后按 Enter 键继续..." # 再次查找 if ! find_appimage; then log_error "在 '$APPIMAGE_SEARCH_DIR' 中仍然找不到 '$APPIMAGE_PATTERN' 文件。安装中止。" return 1 fi fi log_info "找到 AppImage 文件: $found_appimage_path" local appimage_filename=$(basename "$found_appimage_path") # 进入搜索目录操作,避免路径问题 local current_dir=$(pwd) cd "$APPIMAGE_SEARCH_DIR" || { log_error "无法进入目录: $APPIMAGE_SEARCH_DIR"; return 1; } log_info "设置 '$appimage_filename' 可执行权限..." chmod +x "$appimage_filename" || { log_error "设置可执行权限失败: $appimage_filename" cd "$current_dir" return 1 } log_info "解压 AppImage 文件 '$appimage_filename'..." # 创建临时解压目录 local extract_dir="squashfs-root" rm -rf "$extract_dir" # 清理旧的解压目录(如果存在) # 执行解压,将输出重定向避免干扰 if ./"$appimage_filename" --appimage-extract > /dev/null; then log_info "AppImage 解压成功到 '$extract_dir'" else log_error "解压 AppImage 失败: $appimage_filename" rm -rf "$extract_dir" # 清理失败的解压 cd "$current_dir" return 1 fi # 检查解压后的预期目录结构 local cursor_source_dir="" if [ -d "$extract_dir/usr/share/cursor" ]; then cursor_source_dir="$extract_dir/usr/share/cursor" elif [ -d "$extract_dir" ]; then # 有些 AppImage 可能直接在根目录 # 进一步检查是否存在关键文件/目录 if [ -f "$extract_dir/cursor" ] && [ -d "$extract_dir/resources" ]; then cursor_source_dir="$extract_dir" fi fi if [ -z "$cursor_source_dir" ]; then log_error "解压后的目录 '$extract_dir' 中未找到预期的 Cursor 文件结构 (例如 'usr/share/cursor' 或直接包含 'cursor' 和 'resources')。" rm -rf "$extract_dir" cd "$current_dir" return 1 fi log_info "找到 Cursor 源文件在: $cursor_source_dir" log_info "安装 Cursor 到 '$INSTALL_DIR'..." # 如果安装目录已存在,先删除 (确保全新安装) if [ -d "$INSTALL_DIR" ]; then log_warn "发现已存在的安装目录 '$INSTALL_DIR',将先移除..." rm -rf "$INSTALL_DIR" || { log_error "移除旧安装目录失败: $INSTALL_DIR"; cd "$current_dir"; return 1; } fi # 创建安装目录的父目录(如果需要)并设置权限 mkdir -p "$(dirname "$INSTALL_DIR")" # 移动解压后的内容到安装目录 if mv "$cursor_source_dir" "$INSTALL_DIR"; then log_info "成功将文件移动到 '$INSTALL_DIR'" # 确保安装目录及其内容归属当前用户(如果需要) chown -R "$CURRENT_USER":"$CURRENT_GROUP" "$INSTALL_DIR" || log_warn "设置 '$INSTALL_DIR' 文件所有权失败,可能需要手动调整" chmod -R u+rwX,go+rX,go-w "$INSTALL_DIR" || log_warn "设置 '$INSTALL_DIR' 文件权限失败,可能需要手动调整" else log_error "移动文件到安装目录 '$INSTALL_DIR' 失败" rm -rf "$extract_dir" # 确保清理 rm -rf "$INSTALL_DIR" # 清理部分移动的文件 cd "$current_dir" return 1 fi # 处理图标和桌面快捷方式 (从脚本执行的原始目录查找) cd "$current_dir" # 返回原始目录查找图标等文件 local icon_source="./cursor.png" local desktop_source="./cursor-cursor.desktop" if [ -f "$icon_source" ]; then log_info "安装图标..." mkdir -p "$(dirname "$ICON_PATH")" cp "$icon_source" "$ICON_PATH" || log_warn "无法复制图标文件 '$icon_source' 到 '$ICON_PATH'" chmod 644 "$ICON_PATH" || log_warn "设置图标文件权限失败: $ICON_PATH" else log_warn "图标文件 '$icon_source' 在脚本当前目录不存在,跳过图标安装。" log_warn "请将 'cursor.png' 文件放置在脚本目录 '$current_dir' 下并重新运行安装(如果需要图标)。" fi if [ -f "$desktop_source" ]; then log_info "安装桌面快捷方式..." mkdir -p "$(dirname "$DESKTOP_FILE")" cp "$desktop_source" "$DESKTOP_FILE" || log_warn "无法创建桌面快捷方式 '$desktop_source' 到 '$DESKTOP_FILE'" chmod 644 "$DESKTOP_FILE" || log_warn "设置桌面文件权限失败: $DESKTOP_FILE" # 更新桌面数据库 log_info "更新桌面数据库..." update-desktop-database "$(dirname "$DESKTOP_FILE")" &> /dev/null || log_warn "无法更新桌面数据库,快捷方式可能不会立即显示" else log_warn "桌面文件 '$desktop_source' 在脚本当前目录不存在,跳过快捷方式安装。" log_warn "请将 'cursor-cursor.desktop' 文件放置在脚本目录 '$current_dir' 下并重新运行安装(如果需要快捷方式)。" fi # 创建符号链接到 /usr/local/bin log_info "创建命令行启动链接..." ln -sf "$INSTALL_DIR/cursor" /usr/local/bin/cursor || log_warn "无法创建命令行链接 '/usr/local/bin/cursor'" # 清理临时文件 log_info "清理临时文件..." cd "$APPIMAGE_SEARCH_DIR" # 返回搜索目录清理 rm -rf "$extract_dir" log_info "正在删除原始 AppImage 文件: $found_appimage_path" rm -f "$appimage_filename" # 删除 AppImage 文件 cd "$current_dir" # 确保返回最终目录 log_info "Cursor 安装成功!安装目录: $INSTALL_DIR" return 0 } # --- 结束:安装函数 --- # 检查并关闭 Cursor 进程 # 获取 Cursor 相关进程 PID(兼容 pgrep/ps 多种实现) get_cursor_pids() { local self_pid="$$" local pids="" # 优先使用 pgrep(更稳定):仅按进程名匹配,避免误匹配到脚本命令行(例如 sudo bash ...cursor_linux_id_modifier.sh) if command -v pgrep >/dev/null 2>&1; then pids=$(pgrep -i "cursor" 2>/dev/null || true) if [ -z "$pids" ]; then pids=$(pgrep "cursor" 2>/dev/null || true) fi if [ -z "$pids" ]; then pids=$(pgrep "Cursor" 2>/dev/null || true) fi if [ -n "$pids" ]; then echo "$pids" | awk -v self="$self_pid" '$1 ~ /^[0-9]+$/ && $1 != self {print $1}' | sort -u return 0 fi fi # 回退:兼容不同 ps 实现(BusyBox 可能不支持 aux / -ef) if ps aux >/dev/null 2>&1; then ps aux 2>/dev/null \ | grep -i '[c]ursor' \ | grep -v "cursor_linux_id_modifier.sh" \ | awk '{print $2}' \ | awk -v self="$self_pid" '$1 ~ /^[0-9]+$/ && $1 != self {print $1}' \ | sort -u return 0 fi if ps -ef >/dev/null 2>&1; then ps -ef 2>/dev/null \ | grep -i '[c]ursor' \ | grep -v "cursor_linux_id_modifier.sh" \ | awk '{print $2}' \ | awk -v self="$self_pid" '$1 ~ /^[0-9]+$/ && $1 != self {print $1}' \ | sort -u return 0 fi ps 2>/dev/null \ | grep -i '[c]ursor' \ | grep -v "cursor_linux_id_modifier.sh" \ | awk '{print $1}' \ | awk -v self="$self_pid" '$1 ~ /^[0-9]+$/ && $1 != self {print $1}' \ | sort -u return 0 } # 打印 Cursor 相关进程详情(用于排障;不依赖固定列结构) print_cursor_process_details() { log_debug "正在获取 Cursor 进程详细信息:" if ps aux >/dev/null 2>&1; then ps aux 2>/dev/null | grep -i '[c]ursor' | grep -v "cursor_linux_id_modifier.sh" || true return 0 fi if ps -ef >/dev/null 2>&1; then ps -ef 2>/dev/null | grep -i '[c]ursor' | grep -v "cursor_linux_id_modifier.sh" || true return 0 fi ps 2>/dev/null | grep -i '[c]ursor' | grep -v "cursor_linux_id_modifier.sh" || true return 0 } check_and_kill_cursor() { log_info "检查 Cursor 进程..." local attempt=1 local max_attempts=5 while [ $attempt -le $max_attempts ]; do # 跨发行版兼容:优先 pgrep,其次兼容 ps aux/ps -ef/ps 的 PID 列差异 local cursor_pids_raw cursor_pids_raw=$(get_cursor_pids || true) # 将换行分隔的 PID 列表转换为空格分隔,便于传给 kill(避免依赖 xargs) CURSOR_PIDS=$(echo "$cursor_pids_raw" | tr '\n' ' ' | sed 's/[[:space:]][[:space:]]*/ /g; s/^ //; s/ $//' || true) if [ -z "$CURSOR_PIDS" ]; then log_info "未发现运行中的 Cursor 进程" return 0 fi log_warn "发现 Cursor 进程正在运行: $CURSOR_PIDS" print_cursor_process_details log_warn "尝试关闭 Cursor 进程..." if [ $attempt -eq $max_attempts ]; then log_warn "尝试强制终止进程..." kill -9 $CURSOR_PIDS 2>/dev/null || true else kill $CURSOR_PIDS 2>/dev/null || true fi sleep 1 # 再次检查进程是否还在运行 if [ -z "$(get_cursor_pids | head -n 1)" ]; then log_info "Cursor 进程已成功关闭" return 0 fi log_warn "等待进程关闭,尝试 $attempt/$max_attempts..." ((attempt++)) done log_error "在 $max_attempts 次尝试后仍无法关闭 Cursor 进程" print_cursor_process_details log_error "请手动关闭进程后重试" exit 1 } # 备份配置文件 backup_config() { if [ ! -f "$STORAGE_FILE" ]; then log_warn "配置文件 '$STORAGE_FILE' 不存在,跳过备份" return 0 fi mkdir -p "$BACKUP_DIR" local backup_file="$BACKUP_DIR/storage.json.backup_$(date +%Y%m%d_%H%M%S)" if cp "$STORAGE_FILE" "$backup_file"; then chmod 644 "$backup_file" # 确保备份文件归属正确用户 chown "$CURRENT_USER":"$CURRENT_GROUP" "$backup_file" || log_warn "设置备份文件所有权失败: $backup_file" log_info "配置已备份到: $backup_file" else log_error "备份失败: $STORAGE_FILE" exit 1 fi return 0 # 明确返回成功 } # 生成随机 ID generate_hex_bytes() { local bytes="$1" # 优先使用 openssl if command -v openssl >/dev/null 2>&1; then openssl rand -hex "$bytes" return 0 fi # 兜底:/dev/urandom + od(多数发行版可用) if [ -r /dev/urandom ] && command -v od >/dev/null 2>&1; then # 使用更通用的 od 参数写法,兼容更多发行版实现 od -An -N "$bytes" -t x1 /dev/urandom | tr -d ' \n' return 0 fi # 最后兜底:如果 python3 可用 if command -v python3 >/dev/null 2>&1; then python3 -c 'import os, sys; print(os.urandom(int(sys.argv[1])).hex())' "$bytes" return 0 fi log_error "缺少 openssl/od/python3,无法生成随机数(bytes=$bytes)" return 1 } generate_random_id() { # 生成32字节(64个十六进制字符)的随机数 generate_hex_bytes 32 } # 生成随机 UUID generate_uuid() { # 在Linux上使用uuidgen生成UUID if command -v uuidgen &> /dev/null; then uuidgen | tr '[:upper:]' '[:lower:]' else # 备选方案:使用/proc/sys/kernel/random/uuid if [ -f /proc/sys/kernel/random/uuid ]; then cat /proc/sys/kernel/random/uuid else # 最后备选方案:使用随机 16 bytes 并格式化(避免 sed 捕获组超 9 的兼容性问题) local hex hex=$(generate_hex_bytes 16) || return 1 echo "${hex:0:8}-${hex:8:4}-${hex:12:4}-${hex:16:4}-${hex:20:12}" fi fi } # 规范化 machineId(确保为十六进制字符串) normalize_machine_id() { local raw="$1" local cleaned cleaned=$(echo "$raw" | tr -d '-' | tr '[:upper:]' '[:lower:]') if [[ "$cleaned" =~ ^[0-9a-f]{32,}$ ]]; then echo "$cleaned" return 0 fi return 1 } # 从现有配置读取ID(用于JS注入保持一致) load_ids_from_storage() { if [ ! -f "$STORAGE_FILE" ]; then return 1 fi if ! command -v python3 >/dev/null 2>&1; then log_warn "未检测到 python3,无法从现有配置读取 ID" return 1 fi local output output=$(python3 - "$STORAGE_FILE" <<'PY' import json, sys path = sys.argv[1] with open(path, "r", encoding="utf-8") as f: data = json.load(f) def pick(keys): for k in keys: v = data.get(k) if isinstance(v, str) and v: return v return "" items = { "machineId": pick(["telemetry.machineId", "machineId"]), "macMachineId": pick(["telemetry.macMachineId"]), "devDeviceId": pick(["telemetry.devDeviceId", "deviceId"]), "sqmId": pick(["telemetry.sqmId"]), "firstSessionDate": pick(["telemetry.firstSessionDate"]), } for k, v in items.items(): print(f"{k}={v}") PY ) if [ $? -ne 0 ] || [ -z "$output" ]; then return 1 fi while IFS='=' read -r key value; do case "$key" in machineId) CURSOR_ID_MACHINE_ID="$value" ;; macMachineId) CURSOR_ID_MAC_MACHINE_ID="$value" ;; devDeviceId) CURSOR_ID_DEVICE_ID="$value" ;; sqmId) CURSOR_ID_SQM_ID="$value" ;; firstSessionDate) CURSOR_ID_FIRST_SESSION_DATE="$value" ;; esac done <<< "$output" if [ -n "$CURSOR_ID_MACHINE_ID" ]; then local normalized if normalized=$(normalize_machine_id "$CURSOR_ID_MACHINE_ID"); then if [ "$normalized" != "$CURSOR_ID_MACHINE_ID" ]; then log_warn "machineId 非标准格式,JS 注入将使用去除连字符后的值" fi CURSOR_ID_MACHINE_ID="$normalized" else log_warn "machineId 无法识别为十六进制,JS 注入将改用新值" CURSOR_ID_MACHINE_ID="" fi fi CURSOR_ID_SESSION_ID=$(generate_uuid) CURSOR_ID_MAC_ADDRESS="${CURSOR_ID_MAC_ADDRESS:-00:11:22:33:44:55}" if [ -n "$CURSOR_ID_MACHINE_ID" ] && [ -n "$CURSOR_ID_MAC_MACHINE_ID" ] && [ -n "$CURSOR_ID_DEVICE_ID" ] && [ -n "$CURSOR_ID_SQM_ID" ]; then return 0 fi return 1 } # 仅用于JS注入的ID生成(不写配置) generate_ids_for_js_only() { CURSOR_ID_MACHINE_ID=$(generate_random_id) CURSOR_ID_MACHINE_GUID=$(generate_uuid) CURSOR_ID_MAC_MACHINE_ID=$(generate_random_id) CURSOR_ID_DEVICE_ID=$(generate_uuid) CURSOR_ID_SQM_ID="{$(generate_uuid | tr '[:lower:]' '[:upper:]')}" CURSOR_ID_FIRST_SESSION_DATE=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z") CURSOR_ID_SESSION_ID=$(generate_uuid) CURSOR_ID_MAC_ADDRESS="${CURSOR_ID_MAC_ADDRESS:-00:11:22:33:44:55}" } # 修改现有文件 modify_or_add_config() { local key="$1" local value="$2" local file="$3" if [ ! -f "$file" ]; then log_error "配置文件不存在: $file" return 1 fi # 确保文件对当前执行用户(root)可写 chmod u+w "$file" || { log_error "无法修改文件权限(写): $file" return 1 } # 创建临时文件 local temp_file=$(mktemp) # 检查key是否存在 if grep -q "\"$key\":[[:space:]]*\"[^\"]*\"" "$file"; then # key存在,执行替换 (更精确的匹配) sed "s/\\(\"$key\"\\):[[:space:]]*\"[^\"]*\"/\\1: \"$value\"/" "$file" > "$temp_file" || { log_error "修改配置失败 (替换): $key in $file" rm -f "$temp_file" chmod u-w "$file" # 恢复权限 return 1 } log_debug "已替换 key '$key' 在文件 '$file' 中" elif grep -q "}" "$file"; then # key不存在, 在最后一个 '}' 前添加新的key-value对 # 注意:这种方式比较脆弱,如果 JSON 格式不标准或最后一行不是 '}' 会失败 # 🔧 兼容修复:不依赖 GNU sed 的 \n 替换扩展;同时避免在 `}` 独占一行时生成无效 JSON if tail -n 1 "$file" | grep -Eq '^[[:space:]]*}[[:space:]]*$'; then # 多行 JSON:在最后一个 `}` 前插入新行,并为上一条属性补上逗号 awk -v key="$key" -v value="$value" ' { lines[NR] = $0 } END { brace = 0 for (i = NR; i >= 1; i--) { if (lines[i] ~ /^[[:space:]]*}[[:space:]]*$/) { brace = i; break } } if (brace == 0) { exit 2 } prev = 0 for (i = brace - 1; i >= 1; i--) { if (lines[i] !~ /^[[:space:]]*$/) { prev = i; break } } if (prev > 0) { line = lines[prev] sub(/[[:space:]]*$/, "", line) if (line !~ /{$/ && line !~ /,$/) { lines[prev] = line "," } else { lines[prev] = line } } insert_line = " \"" key "\": \"" value "\"" for (i = 1; i <= NR; i++) { if (i == brace) { print insert_line } print lines[i] } } ' "$file" > "$temp_file" || { log_error "添加配置失败 (注入): $key to $file" rm -f "$temp_file" chmod u-w "$file" # 恢复权限 return 1 } else # 单行 JSON:直接在末尾 `}` 前插入键值(避免依赖 sed 的 \\n 扩展) sed "s/}[[:space:]]*$/,\"$key\": \"$value\"}/" "$file" > "$temp_file" || { log_error "添加配置失败 (注入): $key to $file" rm -f "$temp_file" chmod u-w "$file" # 恢复权限 return 1 } fi log_debug "已添加 key '$key' 到文件 '$file' 中" else log_error "无法确定如何添加配置: $key to $file (文件结构可能不标准)" rm -f "$temp_file" chmod u-w "$file" # 恢复权限 return 1 fi # 检查临时文件是否有效 if [ ! -s "$temp_file" ]; then log_error "修改或添加配置后生成的临时文件为空: $key in $file" rm -f "$temp_file" chmod u-w "$file" # 恢复权限 return 1 fi # 使用 cat 替换原文件内容 cat "$temp_file" > "$file" || { log_error "无法写入更新后的配置到文件: $file" rm -f "$temp_file" # 尝试恢复权限(如果失败也无大碍) chmod u-w "$file" || true return 1 } rm -f "$temp_file" # 设置所有者和基础权限(root执行时目标文件是用户家目录下的) chown "$CURRENT_USER":"$CURRENT_GROUP" "$file" || log_warn "设置文件所有权失败: $file" chmod 644 "$file" || log_warn "设置文件权限失败: $file" # 用户读写,组和其他读 return 0 } # 生成新的配置 generate_new_config() { echo log_warn "机器码重置选项" # 使用菜单选择函数询问用户是否重置机器码 set +e # 默认选择“重置”,满足“默认应全部处理”的需求 select_menu_option "是否需要重置机器码? (默认:重置并同步修改配置文件):" "不重置 - 仅修改js文件即可|重置 - 同时修改配置文件和机器码" 1 reset_choice=$? set -e # 记录日志以便调试 echo "[INPUT_DEBUG] 机器码重置选项选择: $reset_choice" >> "$LOG_FILE" # 确保配置文件目录存在 mkdir -p "$(dirname "$STORAGE_FILE")" chown "$CURRENT_USER":"$CURRENT_GROUP" "$(dirname "$STORAGE_FILE")" || log_warn "设置配置目录所有权失败: $(dirname "$STORAGE_FILE")" chmod 755 "$(dirname "$STORAGE_FILE")" || log_warn "设置配置目录权限失败: $(dirname "$STORAGE_FILE")" # 处理用户选择 - 索引0对应"不重置"选项,索引1对应"重置"选项 if [ "$reset_choice" = "1" ]; then log_info "您选择了重置机器码" # 检查配置文件是否存在 if [ -f "$STORAGE_FILE" ]; then log_info "发现已有配置文件: $STORAGE_FILE" # 备份现有配置 if ! backup_config; then # 如果备份失败,不继续修改 log_error "配置文件备份失败,中止机器码重置。" return 1 # 返回错误状态 fi # 生成并设置新的设备ID local new_device_id=$(generate_uuid) local new_machine_id=$(generate_random_id) # 🔧 新增: serviceMachineId (用于 storage.serviceMachineId) local new_service_machine_id=$(generate_uuid) # 🔧 新增: firstSessionDate (重置首次会话日期) local new_first_session_date=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z") # 🔧 新增: macMachineId 和 sqmId local new_mac_machine_id=$(generate_random_id) local new_sqm_id="{$(generate_uuid | tr '[:lower:]' '[:upper:]')}" CURSOR_ID_MACHINE_ID="$new_machine_id" CURSOR_ID_MAC_MACHINE_ID="$new_mac_machine_id" CURSOR_ID_DEVICE_ID="$new_device_id" CURSOR_ID_SQM_ID="$new_sqm_id" CURSOR_ID_FIRST_SESSION_DATE="$new_first_session_date" CURSOR_ID_SESSION_ID=$(generate_uuid) CURSOR_ID_MAC_ADDRESS="${CURSOR_ID_MAC_ADDRESS:-00:11:22:33:44:55}" log_info "正在设置新的设备和机器ID..." log_debug "新设备ID: $new_device_id" log_debug "新机器ID: $new_machine_id" log_debug "新serviceMachineId: $new_service_machine_id" log_debug "新firstSessionDate: $new_first_session_date" # 修改配置文件 # 🔧 修复: 添加 storage.serviceMachineId, telemetry.firstSessionDate, telemetry.macMachineId, telemetry.sqmId local config_success=true modify_or_add_config "deviceId" "$new_device_id" "$STORAGE_FILE" || config_success=false modify_or_add_config "machineId" "$new_machine_id" "$STORAGE_FILE" || config_success=false modify_or_add_config "telemetry.machineId" "$new_machine_id" "$STORAGE_FILE" || config_success=false modify_or_add_config "telemetry.macMachineId" "$new_mac_machine_id" "$STORAGE_FILE" || config_success=false modify_or_add_config "telemetry.devDeviceId" "$new_device_id" "$STORAGE_FILE" || config_success=false modify_or_add_config "telemetry.sqmId" "$new_sqm_id" "$STORAGE_FILE" || config_success=false modify_or_add_config "storage.serviceMachineId" "$new_service_machine_id" "$STORAGE_FILE" || config_success=false modify_or_add_config "telemetry.firstSessionDate" "$new_first_session_date" "$STORAGE_FILE" || config_success=false if [ "$config_success" = true ]; then log_info "配置文件中的所有标识符修改成功" log_info "📋 [详情] 已更新以下标识符:" echo " 🔹 deviceId: ${new_device_id:0:16}..." echo " 🔹 machineId: ${new_machine_id:0:16}..." echo " 🔹 macMachineId: ${new_mac_machine_id:0:16}..." echo " 🔹 sqmId: $new_sqm_id" echo " 🔹 serviceMachineId: $new_service_machine_id" echo " 🔹 firstSessionDate: $new_first_session_date" # 🔧 新增: 修改 machineid 文件 log_info "🔧 [machineid] 正在修改 machineid 文件..." local machineid_file_path="$CURSOR_CONFIG_DIR/machineid" if [ -f "$machineid_file_path" ]; then # 备份原始 machineid 文件 local machineid_backup="$BACKUP_DIR/machineid.backup_$(date +%Y%m%d_%H%M%S)" cp "$machineid_file_path" "$machineid_backup" 2>/dev/null && \ log_info "💾 [备份] machineid 文件已备份: $machineid_backup" fi # 写入新的 serviceMachineId 到 machineid 文件 if echo -n "$new_service_machine_id" > "$machineid_file_path" 2>/dev/null; then log_info "✅ [machineid] machineid 文件修改成功: $new_service_machine_id" # 设置 machineid 文件为只读 chmod 444 "$machineid_file_path" 2>/dev/null && \ log_info "🔒 [保护] machineid 文件已设置为只读" else log_warn "⚠️ [machineid] machineid 文件修改失败" log_info "💡 [提示] 可手动修改文件: $machineid_file_path" fi # 🔧 新增: 修改 .updaterId 文件(更新器设备标识符) log_info "🔧 [updaterId] 正在修改 .updaterId 文件..." local updater_id_file_path="$CURSOR_CONFIG_DIR/.updaterId" if [ -f "$updater_id_file_path" ]; then # 备份原始 .updaterId 文件 local updater_id_backup="$BACKUP_DIR/.updaterId.backup_$(date +%Y%m%d_%H%M%S)" cp "$updater_id_file_path" "$updater_id_backup" 2>/dev/null && \ log_info "💾 [备份] .updaterId 文件已备份: $updater_id_backup" fi # 生成新的 updaterId(UUID格式) local new_updater_id=$(generate_uuid) if echo -n "$new_updater_id" > "$updater_id_file_path" 2>/dev/null; then log_info "✅ [updaterId] .updaterId 文件修改成功: $new_updater_id" # 设置 .updaterId 文件为只读 chmod 444 "$updater_id_file_path" 2>/dev/null && \ log_info "🔒 [保护] .updaterId 文件已设置为只读" else log_warn "⚠️ [updaterId] .updaterId 文件修改失败" log_info "💡 [提示] 可手动修改文件: $updater_id_file_path" fi else log_error "配置文件中的部分标识符修改失败" # 注意:即使失败,备份仍在,但配置文件可能已部分修改 return 1 # 返回错误状态 fi else log_warn "未找到配置文件 '$STORAGE_FILE',无法重置机器码。如果这是首次安装,这是正常的。" # 即使文件不存在,也认为此步骤(不执行)是"成功"的,允许继续 fi else log_info "您选择了不重置机器码,将仅修改js文件" # 检查配置文件是否存在并备份(如果存在) if [ -f "$STORAGE_FILE" ]; then log_info "发现已有配置文件: $STORAGE_FILE" if ! backup_config; then log_error "配置文件备份失败,中止操作。" return 1 # 返回错误状态 fi if load_ids_from_storage; then log_info "已从现有配置读取 ID,JS 注入将保持一致" else log_warn "无法从现有配置读取 ID,JS 注入将使用新生成的 ID(不会修改配置)" generate_ids_for_js_only fi else log_warn "未找到配置文件 '$STORAGE_FILE',跳过备份。" log_warn "无法读取现有ID,JS 注入将使用新生成的 ID(不会修改配置)" generate_ids_for_js_only fi fi echo log_info "配置处理完成" return 0 # 明确返回成功 } # 查找Cursor的JS文件 find_cursor_js_files() { log_info "查找Cursor的JS文件..." local js_files=() local found=false # 确保 CURSOR_RESOURCES 已设置 if [ -z "$CURSOR_RESOURCES" ] || [ ! -d "$CURSOR_RESOURCES" ]; then log_error "Cursor 资源目录未找到或无效 ($CURSOR_RESOURCES),无法查找 JS 文件。" return 1 fi log_debug "在资源目录中搜索JS文件: $CURSOR_RESOURCES" # 在资源目录中递归搜索特定JS文件 # 注意:这些模式可能需要根据 Cursor 版本更新 local js_patterns=( "resources/app/out/vs/workbench/api/node/extensionHostProcess.js" "resources/app/out/main.js" "resources/app/out/vs/code/electron-utility/sharedProcess/sharedProcessMain.js" "resources/app/out/vs/code/node/cliProcessMain.js" # 添加其他可能的路径模式 "app/out/vs/workbench/api/node/extensionHostProcess.js" # 如果资源目录是 app 的父目录 "app/out/main.js" "app/out/vs/code/electron-utility/sharedProcess/sharedProcessMain.js" "app/out/vs/code/node/cliProcessMain.js" ) for pattern in "${js_patterns[@]}"; do # 使用 find 在 CURSOR_RESOURCES 下查找完整路径 # 兼容修复:find 遇到错误返回非0可能触发 set -e,这里统一兜底为成功返回 local files=$(find "$CURSOR_RESOURCES" -path "*/$pattern" -type f 2>/dev/null || true) if [ -n "$files" ]; then while IFS= read -r file; do # 检查文件是否已添加 if [[ ! " ${js_files[@]} " =~ " ${file} " ]]; then log_info "找到JS文件: $file" js_files+=("$file") found=true fi done <<< "$files" fi done # 如果还没找到,尝试更通用的搜索(可能误报) if [ "$found" = false ]; then log_warn "在标准路径模式中未找到JS文件,尝试在资源目录 '$CURSOR_RESOURCES' 中进行更广泛的搜索..." # 查找包含特定关键字的 JS 文件 local files=$(find "$CURSOR_RESOURCES" -name "*.js" -type f -exec grep -lE 'IOPlatformUUID|x-cursor-checksum|getMachineId' {} \; 2>/dev/null || true) if [ -n "$files" ]; then while IFS= read -r file; do if [[ ! " ${js_files[@]} " =~ " ${file} " ]]; then log_info "通过关键字找到可能的JS文件: $file" js_files+=("$file") found=true fi done <<< "$files" else log_warn "在资源目录 '$CURSOR_RESOURCES' 中通过关键字也未能找到 JS 文件。" fi fi if [ "$found" = false ]; then log_error "在资源目录 '$CURSOR_RESOURCES' 中未找到任何可修改的JS文件。" log_error "请检查 Cursor 安装是否完整,或脚本中的 JS 路径模式是否需要更新。" return 1 fi # 去重(理论上上面的检查已经处理,但以防万一) IFS=" " read -r -a CURSOR_JS_FILES <<< "$(echo "${js_files[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')" log_info "找到 ${#CURSOR_JS_FILES[@]} 个唯一的JS文件需要处理。" return 0 } # 修改Cursor的JS文件 # 🔧 修改Cursor内核JS文件实现设备识别绕过(增强版三重方案) # 方案A: someValue占位符替换 - 稳定锚点,不依赖混淆后的函数名 # 方案B: b6 定点重写 - 机器码源函数直接返回固定值 # 方案C: Loader Stub + 外置 Hook - 主/共享进程仅加载外置 Hook 文件 modify_cursor_js_files() { log_info "🔧 [内核修改] 开始修改Cursor内核JS文件实现设备识别绕过..." log_info "💡 [方案] 使用增强版三重方案:占位符替换 + b6 定点重写 + Loader Stub + 外置 Hook" # 先查找需要修改的JS文件 if ! find_cursor_js_files; then return 1 fi if [ ${#CURSOR_JS_FILES[@]} -eq 0 ]; then log_error "JS 文件列表为空,无法继续修改。" return 1 fi # 生成或复用设备标识符(优先使用配置中读取的值) local machine_id="${CURSOR_ID_MACHINE_ID:-}" local machine_guid="${CURSOR_ID_MACHINE_GUID:-}" local device_id="${CURSOR_ID_DEVICE_ID:-}" local mac_machine_id="${CURSOR_ID_MAC_MACHINE_ID:-}" local sqm_id="${CURSOR_ID_SQM_ID:-}" local session_id="${CURSOR_ID_SESSION_ID:-}" local first_session_date="${CURSOR_ID_FIRST_SESSION_DATE:-}" local mac_address="${CURSOR_ID_MAC_ADDRESS:-00:11:22:33:44:55}" local ids_missing=false if [ -z "$machine_id" ]; then machine_id=$(generate_random_id) ids_missing=true fi if [ -z "$machine_guid" ]; then machine_guid=$(generate_uuid) ids_missing=true fi if [ -z "$device_id" ]; then device_id=$(generate_uuid) ids_missing=true fi if [ -z "$mac_machine_id" ]; then mac_machine_id=$(generate_random_id) ids_missing=true fi if [ -z "$sqm_id" ]; then sqm_id="{$(generate_uuid | tr '[:lower:]' '[:upper:]')}" ids_missing=true fi if [ -z "$session_id" ]; then session_id=$(generate_uuid) ids_missing=true fi if [ -z "$first_session_date" ]; then first_session_date=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z") ids_missing=true fi if [ "$ids_missing" = true ]; then log_warn "部分 ID 未从配置获取,已生成新值用于 JS 注入" else log_info "已使用配置中的设备标识符进行 JS 注入" fi CURSOR_ID_MACHINE_ID="$machine_id" CURSOR_ID_MACHINE_GUID="$machine_guid" CURSOR_ID_DEVICE_ID="$device_id" CURSOR_ID_MAC_MACHINE_ID="$mac_machine_id" CURSOR_ID_SQM_ID="$sqm_id" CURSOR_ID_SESSION_ID="$session_id" CURSOR_ID_FIRST_SESSION_DATE="$first_session_date" CURSOR_ID_MAC_ADDRESS="$mac_address" log_info "🔑 [准备] 设备标识符已就绪" log_info " machineId: ${machine_id:0:16}..." log_info " machineGuid: ${machine_guid:0:16}..." log_info " deviceId: ${device_id:0:16}..." log_info " macMachineId: ${mac_machine_id:0:16}..." log_info " sqmId: $sqm_id" # 每次执行都删除旧配置并重新生成,确保获得新的设备标识符 local ids_config_path="$TARGET_HOME/.cursor_ids.json" if [ -f "$ids_config_path" ]; then rm -f "$ids_config_path" log_info "🗑️ [清理] 已删除旧的 ID 配置文件" fi cat > "$ids_config_path" << EOF { "machineId": "$machine_id", "machineGuid": "$machine_guid", "macMachineId": "$mac_machine_id", "devDeviceId": "$device_id", "sqmId": "$sqm_id", "macAddress": "$mac_address", "sessionId": "$session_id", "firstSessionDate": "$first_session_date", "createdAt": "$first_session_date" } EOF chown "$CURRENT_USER":"$CURRENT_GROUP" "$ids_config_path" 2>/dev/null || true log_info "💾 [保存] 新的 ID 配置已保存到: $ids_config_path" # 部署外置 Hook 文件(供 Loader Stub 加载,支持多域名备用下载) local hook_target_path="$TARGET_HOME/.cursor_hook.js" local script_dir script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" local hook_source_path="$script_dir/../hook/cursor_hook.js" local hook_download_urls=( "https://wget.la/https://raw.githubusercontent.com/yuaotian/go-cursor-help/refs/heads/master/scripts/hook/cursor_hook.js" "https://down.npee.cn/?https://raw.githubusercontent.com/yuaotian/go-cursor-help/refs/heads/master/scripts/hook/cursor_hook.js" "https://xget.xi-xu.me/gh/yuaotian/go-cursor-help/refs/heads/master/scripts/hook/cursor_hook.js" "https://gh-proxy.com/https://raw.githubusercontent.com/yuaotian/go-cursor-help/refs/heads/master/scripts/hook/cursor_hook.js" "https://gh.chjina.com/https://raw.githubusercontent.com/yuaotian/go-cursor-help/refs/heads/master/scripts/hook/cursor_hook.js" ) # 支持通过环境变量覆盖下载节点(逗号分隔) if [ -n "$CURSOR_HOOK_DOWNLOAD_URLS" ]; then IFS=',' read -r -a hook_download_urls <<< "$CURSOR_HOOK_DOWNLOAD_URLS" log_info "ℹ️ [Hook] 检测到自定义下载节点列表,将优先使用" fi if [ -f "$hook_source_path" ]; then if cp "$hook_source_path" "$hook_target_path"; then chown "$CURRENT_USER":"$CURRENT_GROUP" "$hook_target_path" 2>/dev/null || true log_info "✅ [Hook] 外置 Hook 已部署: $hook_target_path" else log_warn "⚠️ [Hook] 本地 Hook 复制失败,尝试在线下载..." fi fi if [ ! -f "$hook_target_path" ]; then log_info "ℹ️ [Hook] 正在下载外置 Hook,用于设备标识拦截..." local hook_downloaded=false local total_urls=${#hook_download_urls[@]} if [ "$total_urls" -eq 0 ]; then log_warn "⚠️ [Hook] 下载节点列表为空,跳过在线下载" elif command -v curl >/dev/null 2>&1; then local index=0 for url in "${hook_download_urls[@]}"; do index=$((index + 1)) log_info "⏳ [Hook] ($index/$total_urls) 当前下载节点: $url" # 兼容修复:部分 curl 版本可能不支持 --progress-bar,失败时回退为基础参数 if curl -fL --progress-bar "$url" -o "$hook_target_path"; then chown "$CURRENT_USER":"$CURRENT_GROUP" "$hook_target_path" 2>/dev/null || true log_info "✅ [Hook] 外置 Hook 已在线下载: $hook_target_path" hook_downloaded=true break fi rm -f "$hook_target_path" log_warn "⚠️ [Hook] curl 下载失败,尝试回退参数重试: $url" if curl -fL "$url" -o "$hook_target_path"; then chown "$CURRENT_USER":"$CURRENT_GROUP" "$hook_target_path" 2>/dev/null || true log_info "✅ [Hook] 外置 Hook 已在线下载: $hook_target_path" hook_downloaded=true break fi rm -f "$hook_target_path" log_warn "⚠️ [Hook] 外置 Hook 下载失败: $url" done elif command -v wget >/dev/null 2>&1; then local index=0 for url in "${hook_download_urls[@]}"; do index=$((index + 1)) log_info "⏳ [Hook] ($index/$total_urls) 当前下载节点: $url" # 兼容修复:BusyBox/精简版 wget 可能不支持 --progress=bar:force,失败时回退为基础参数 if wget --progress=bar:force -O "$hook_target_path" "$url"; then chown "$CURRENT_USER":"$CURRENT_GROUP" "$hook_target_path" 2>/dev/null || true log_info "✅ [Hook] 外置 Hook 已在线下载: $hook_target_path" hook_downloaded=true break fi rm -f "$hook_target_path" log_warn "⚠️ [Hook] wget 下载失败,尝试回退参数重试: $url" if wget -O "$hook_target_path" "$url"; then chown "$CURRENT_USER":"$CURRENT_GROUP" "$hook_target_path" 2>/dev/null || true log_info "✅ [Hook] 外置 Hook 已在线下载: $hook_target_path" hook_downloaded=true break fi rm -f "$hook_target_path" log_warn "⚠️ [Hook] 外置 Hook 下载失败: $url" done else log_warn "⚠️ [Hook] 未检测到 curl/wget,无法在线下载 Hook" fi if [ "$hook_downloaded" != true ] && [ ! -f "$hook_target_path" ]; then log_warn "⚠️ [Hook] 外置 Hook 全部下载失败" fi fi local modified_count=0 local file_modification_status=() # 处理每个文件:创建原始备份或从原始备份恢复 for file in "${CURSOR_JS_FILES[@]}"; do log_info "📝 [处理] 正在处理: $(basename "$file")" if [ ! -f "$file" ]; then log_error "文件不存在: $file,跳过处理。" file_modification_status+=("'$(basename "$file")': Not Found") continue fi # 创建备份目录 local backup_dir="$(dirname "$file")/backups" mkdir -p "$backup_dir" 2>/dev/null || true local file_name=$(basename "$file") local original_backup="$backup_dir/$file_name.original" # 如果原始备份不存在,先创建 if [ ! -f "$original_backup" ]; then # 检查当前文件是否已被修改过 if grep -q "__cursor_patched__" "$file" 2>/dev/null; then log_warn "⚠️ [警告] 文件已被修改但无原始备份,将使用当前版本作为基础" fi cp "$file" "$original_backup" chown "$CURRENT_USER":"$CURRENT_GROUP" "$original_backup" 2>/dev/null || true chmod 444 "$original_backup" 2>/dev/null || true log_info "✅ [备份] 原始备份创建成功: $file_name" else # 从原始备份恢复,确保每次都是干净的注入 log_info "🔄 [恢复] 从原始备份恢复: $file_name" cp "$original_backup" "$file" fi # 创建时间戳备份(记录每次修改前的状态) local backup_file="$backup_dir/$file_name.backup_$(date +%Y%m%d_%H%M%S)" if ! cp "$file" "$backup_file"; then log_error "无法创建文件备份: $file" file_modification_status+=("'$(basename "$file")': Backup Failed") continue fi chown "$CURRENT_USER":"$CURRENT_GROUP" "$backup_file" 2>/dev/null || true chmod 444 "$backup_file" 2>/dev/null || true chmod u+w "$file" || { log_error "无法修改文件权限(写): $file" file_modification_status+=("'$(basename "$file")': Permission Error") continue } local replaced=false # ========== 方法A: someValue占位符替换(稳定锚点) ========== # 重要说明: # 当前 Cursor 的 main.js 中占位符通常是以字符串字面量形式出现,例如: # this.machineId="someValue.machineId" # 如果直接把 someValue.machineId 替换成 "\"<真实值>\"",会形成 ""<真实值>"" 导致 JS 语法错误。 # 因此这里优先替换完整的字符串字面量(包含外层引号),再兜底替换不带引号的占位符。 if grep -q 'someValue\.machineId' "$file"; then sed_inplace "s/\"someValue\.machineId\"/\"${machine_id}\"/g" "$file" sed_inplace "s/'someValue\.machineId'/\"${machine_id}\"/g" "$file" sed_inplace "s/someValue\.machineId/\"${machine_id}\"/g" "$file" log_info " ✓ [方案A] 替换 someValue.machineId" replaced=true fi if grep -q 'someValue\.macMachineId' "$file"; then sed_inplace "s/\"someValue\.macMachineId\"/\"${mac_machine_id}\"/g" "$file" sed_inplace "s/'someValue\.macMachineId'/\"${mac_machine_id}\"/g" "$file" sed_inplace "s/someValue\.macMachineId/\"${mac_machine_id}\"/g" "$file" log_info " ✓ [方案A] 替换 someValue.macMachineId" replaced=true fi if grep -q 'someValue\.devDeviceId' "$file"; then sed_inplace "s/\"someValue\.devDeviceId\"/\"${device_id}\"/g" "$file" sed_inplace "s/'someValue\.devDeviceId'/\"${device_id}\"/g" "$file" sed_inplace "s/someValue\.devDeviceId/\"${device_id}\"/g" "$file" log_info " ✓ [方案A] 替换 someValue.devDeviceId" replaced=true fi if grep -q 'someValue\.sqmId' "$file"; then sed_inplace "s/\"someValue\.sqmId\"/\"${sqm_id}\"/g" "$file" sed_inplace "s/'someValue\.sqmId'/\"${sqm_id}\"/g" "$file" sed_inplace "s/someValue\.sqmId/\"${sqm_id}\"/g" "$file" log_info " ✓ [方案A] 替换 someValue.sqmId" replaced=true fi if grep -q 'someValue\.sessionId' "$file"; then sed_inplace "s/\"someValue\.sessionId\"/\"${session_id}\"/g" "$file" sed_inplace "s/'someValue\.sessionId'/\"${session_id}\"/g" "$file" sed_inplace "s/someValue\.sessionId/\"${session_id}\"/g" "$file" log_info " ✓ [方案A] 替换 someValue.sessionId" replaced=true fi if grep -q 'someValue\.firstSessionDate' "$file"; then sed_inplace "s/\"someValue\.firstSessionDate\"/\"${first_session_date}\"/g" "$file" sed_inplace "s/'someValue\.firstSessionDate'/\"${first_session_date}\"/g" "$file" sed_inplace "s/someValue\.firstSessionDate/\"${first_session_date}\"/g" "$file" log_info " ✓ [方案A] 替换 someValue.firstSessionDate" replaced=true fi # ========== 方法B: b6 定点重写(机器码源函数,仅 main.js) ========== local b6_patched=false if [ "$(basename "$file")" = "main.js" ]; then if command -v python3 >/dev/null 2>&1; then local b6_result b6_result=$(python3 - "$file" "$machine_guid" "$machine_id" <<'PY' if True: import re, sys def diag(msg): print(f"[方案B][诊断] {msg}", file=sys.stderr) path, machine_guid, machine_id = sys.argv[1], sys.argv[2], sys.argv[3] with open(path, "r", encoding="utf-8") as f: data = f.read() # ✅ 1+3 融合:限定 out-build/vs/base/node/id.js 模块内做特征匹配 + 花括号配对定位函数边界 marker = "out-build/vs/base/node/id.js" marker_index = data.find(marker) if marker_index < 0: print("NOT_FOUND") diag(f"未找到模块标记: {marker}") raise SystemExit(0) window_end = min(len(data), marker_index + 200000) window = data[marker_index:window_end] def find_matching_brace(text, open_index, max_scan=20000): limit = min(len(text), open_index + max_scan) depth = 1 in_single = in_double = in_template = False in_line_comment = in_block_comment = False escape = False i = open_index + 1 while i < limit: ch = text[i] nxt = text[i + 1] if i + 1 < limit else "" if in_line_comment: if ch == "\n": in_line_comment = False i += 1 continue if in_block_comment: if ch == "*" and nxt == "/": in_block_comment = False i += 2 continue i += 1 continue if in_single: if escape: escape = False elif ch == "\\\\": escape = True elif ch == "'": in_single = False i += 1 continue if in_double: if escape: escape = False elif ch == "\\\\": escape = True elif ch == '"': in_double = False i += 1 continue if in_template: if escape: escape = False elif ch == "\\\\": escape = True elif ch == "`": in_template = False i += 1 continue if ch == "/" and nxt == "/": in_line_comment = True i += 2 continue if ch == "/" and nxt == "*": in_block_comment = True i += 2 continue if ch == "'": in_single = True i += 1 continue if ch == '"': in_double = True i += 1 continue if ch == "`": in_template = True i += 1 continue if ch == "{": depth += 1 elif ch == "}": depth -= 1 if depth == 0: return i i += 1 return None # 🔧 修复:避免 raw string + 单引号 + ['"] 字符组导致的语法错误;同时修正正则转义,提升 b6 特征匹配命中率 hash_re = re.compile(r"""createHash\(["']sha256["']\)""") sig_re = re.compile(r'^async function (\w+)\((\w+)\)') hash_matches = list(hash_re.finditer(window)) diag(f"marker_index={marker_index} window_len={len(window)} sha256_createHash={len(hash_matches)}") for idx, hm in enumerate(hash_matches, start=1): hash_pos = hm.start() func_start = window.rfind("async function", 0, hash_pos) if func_start < 0: if idx <= 3: diag(f"候选#{idx}: 未找到 async function 起点") continue open_brace = window.find("{", func_start) if open_brace < 0: if idx <= 3: diag(f"候选#{idx}: 未找到函数起始花括号") continue end_brace = find_matching_brace(window, open_brace, max_scan=20000) if end_brace is None: if idx <= 3: diag(f"候选#{idx}: 花括号配对失败(扫描上限内未闭合)") continue func_text = window[func_start:end_brace + 1] if len(func_text) > 8000: if idx <= 3: diag(f"候选#{idx}: 函数体过长 len={len(func_text)},已跳过") continue sm = sig_re.match(func_text) if not sm: if idx <= 3: diag(f"候选#{idx}: 未解析到函数签名(async function name(param))") continue name, param = sm.group(1), sm.group(2) # 特征校验:sha256 + hex digest + return param ? raw : hash has_digest = re.search(r"""\.digest\(["']hex["']\)""", func_text) is not None has_return = re.search(r'return\s+' + re.escape(param) + r'\?\w+:\w+\}', func_text) is not None if idx <= 3: diag(f"候选#{idx}: {name}({param}) len={len(func_text)} digest={has_digest} return={has_return}") if not has_digest: continue if not has_return: continue replacement = f'async function {name}({param}){{return {param}?"{machine_guid}":"{machine_id}";}}' abs_start = marker_index + func_start abs_end = marker_index + end_brace new_data = data[:abs_start] + replacement + data[abs_end + 1:] with open(path, "w", encoding="utf-8") as f: f.write(new_data) diag(f"命中并重写: {name}({param}) len={len(func_text)}") print("PATCHED") break else: diag("未找到满足特征的候选函数") print("NOT_FOUND") PY ) if [ "$b6_result" = "PATCHED" ]; then log_info " ✓ [方案B] 已重写 b6 特征函数" b6_patched=true else log_warn "⚠️ [方案B] 未定位到 b6 特征函数" fi else log_warn "⚠️ [方案B] 未检测到 python3,跳过 b6 定点重写" fi fi # ========== 方法C: Loader Stub 注入 ========== local inject_code='// ========== Cursor Hook Loader 开始 ========== ;(async function(){/*__cursor_patched__*/ "use strict"; if(globalThis.__cursor_hook_loaded__)return; globalThis.__cursor_hook_loaded__=true; try{ // 兼容 ESM/CJS:避免使用 import.meta(仅 ESM 支持),统一用动态 import 加载 Hook var fsMod=await import("fs"); var pathMod=await import("path"); var osMod=await import("os"); var urlMod=await import("url"); var fs=fsMod&&(fsMod.default||fsMod); var path=pathMod&&(pathMod.default||pathMod); var os=osMod&&(osMod.default||osMod); var url=urlMod&&(urlMod.default||urlMod); if(fs&&path&&os&&url&&typeof url.pathToFileURL==="function"){ var hookPath=path.join(os.homedir(), ".cursor_hook.js"); if(typeof fs.existsSync==="function"&&fs.existsSync(hookPath)){ await import(url.pathToFileURL(hookPath).href); } } }catch(e){ // 失败静默,避免影响启动 } })(); // ========== Cursor Hook Loader 结束 ========== ' # 在版权声明后注入代码 local temp_file=$(mktemp) if grep -q '\*/' "$file"; then awk -v inject="$inject_code" ' /\*\// && !injected { print print "" print inject injected = 1 next } { print } ' "$file" > "$temp_file" log_info " ✓ [方案C] Loader Stub 已注入(版权声明后)" else echo "$inject_code" > "$temp_file" cat "$file" >> "$temp_file" log_info " ✓ [方案C] Loader Stub 已注入(文件开头)" fi if mv "$temp_file" "$file"; then local summary="Hook加载器" if [ "$replaced" = true ]; then summary="someValue替换 + $summary" fi if [ "$b6_patched" = true ]; then summary="b6定点重写 + $summary" fi log_info "✅ [成功] 增强版方案修改成功($summary)" ((modified_count++)) file_modification_status+=("'$(basename "$file")': Success") chmod u-w,go-w "$file" 2>/dev/null || true chown "$CURRENT_USER":"$CURRENT_GROUP" "$file" 2>/dev/null || true else log_error "Hook注入失败 (无法移动临时文件)" rm -f "$temp_file" file_modification_status+=("'$(basename "$file")': Inject Failed") cp "$original_backup" "$file" 2>/dev/null || true fi done log_info "📊 [统计] JS 文件处理状态汇总:" for status in "${file_modification_status[@]}"; do log_info " - $status" done if [ "$modified_count" -eq 0 ]; then log_error "❌ [失败] 未能成功修改任何JS文件。" return 1 fi log_info "🎉 [完成] 成功修改 $modified_count 个JS文件" log_info "💡 [说明] 使用增强版三重方案:" log_info " • 方案A: someValue占位符替换(稳定锚点,跨版本兼容)" log_info " • 方案B: b6 定点重写(机器码源函数)" log_info " • 方案C: Loader Stub + 外置 Hook(cursor_hook.js)" log_info "📁 [配置] ID 配置文件: $ids_config_path" return 0 } # 禁用自动更新 disable_auto_update() { log_info "正在尝试禁用 Cursor 自动更新..." # 查找可能的更新配置文件 local update_configs=() # 用户配置目录下的 if [ -d "$CURSOR_CONFIG_DIR" ]; then update_configs+=("$CURSOR_CONFIG_DIR/update-config.json") update_configs+=("$CURSOR_CONFIG_DIR/settings.json") # 有些设置可能在这里 fi # 安装目录下的 (如果资源目录确定) if [ -n "$CURSOR_RESOURCES" ] && [ -d "$CURSOR_RESOURCES" ]; then update_configs+=("$CURSOR_RESOURCES/resources/app-update.yml") update_configs+=("$CURSOR_RESOURCES/app-update.yml") # 可能的位置 fi # 标准安装目录下的 if [ -d "$INSTALL_DIR" ]; then update_configs+=("$INSTALL_DIR/resources/app-update.yml") update_configs+=("$INSTALL_DIR/app-update.yml") fi # $TARGET_HOME/.local/share update_configs+=("$TARGET_HOME/.local/share/cursor/update-config.json") local disabled_count=0 # 处理 JSON 配置文件 local json_config_pattern='update-config.json|settings.json' for config in "${update_configs[@]}"; do if [[ "$config" =~ $json_config_pattern ]] && [ -f "$config" ]; then log_info "找到可能的更新配置文件: $config" # 备份 cp "$config" "${config}.bak_$(date +%Y%m%d%H%M%S)" 2>/dev/null # 尝试修改 JSON (如果存在且是 settings.json) if [[ "$config" == *settings.json ]]; then # 🔧 兼容修复:复用 modify_or_add_config 统一处理替换/注入,避免 sed -i 与 \n 扩展差异 if modify_or_add_config "update.mode" "none" "$config"; then ((disabled_count++)) log_info "已尝试在 '$config' 中设置 'update.mode' 为 'none'" else log_warn "修改 settings.json 中的 update.mode 失败: $config" fi elif [[ "$config" == *update-config.json ]]; then # 直接覆盖 update-config.json echo '{"autoCheck": false, "autoDownload": false}' > "$config" chown "$CURRENT_USER":"$CURRENT_GROUP" "$config" || log_warn "设置所有权失败: $config" chmod 644 "$config" || log_warn "设置权限失败: $config" ((disabled_count++)) log_info "已覆盖更新配置文件: $config" fi fi done # 处理 YAML 配置文件 local yml_config_pattern='app-update.yml' for config in "${update_configs[@]}"; do if [[ "$config" =~ $yml_config_pattern ]] && [ -f "$config" ]; then log_info "找到可能的更新配置文件: $config" # 备份 cp "$config" "${config}.bak_$(date +%Y%m%d%H%M%S)" 2>/dev/null # 清空或修改内容 (简单起见,直接清空或写入禁用标记) echo "# Automatic updates disabled by script $(date)" > "$config" # echo "provider: generic" > "$config" # 或者尝试修改 provider # echo "url: http://127.0.0.1" >> "$config" chmod 444 "$config" # 设置为只读 ((disabled_count++)) log_info "已修改/清空更新配置文件: $config" fi done # 尝试查找updater可执行文件并禁用(重命名或移除权限) local updater_paths=() if [ -n "$CURSOR_RESOURCES" ] && [ -d "$CURSOR_RESOURCES" ]; then # 兼容修复:不强依赖 find -executable,且兜底避免 find 非0 触发 set -e updater_paths+=($(find "$CURSOR_RESOURCES" -name "updater" -type f 2>/dev/null || true)) updater_paths+=($(find "$CURSOR_RESOURCES" -name "CursorUpdater" -type f 2>/dev/null || true)) # macOS 风格? fi if [ -d "$INSTALL_DIR" ]; then updater_paths+=($(find "$INSTALL_DIR" -name "updater" -type f 2>/dev/null || true)) updater_paths+=($(find "$INSTALL_DIR" -name "CursorUpdater" -type f 2>/dev/null || true)) fi updater_paths+=("$CURSOR_CONFIG_DIR/updater") # 旧位置? for updater in "${updater_paths[@]}"; do if [ -f "$updater" ] && [ -x "$updater" ]; then log_info "找到更新程序: $updater" local bak_updater="${updater}.bak_$(date +%Y%m%d%H%M%S)" if mv "$updater" "$bak_updater"; then log_info "已重命名更新程序为: $bak_updater" ((disabled_count++)) else log_warn "重命名更新程序失败: $updater,尝试移除执行权限..." if chmod a-x "$updater"; then log_info "已移除更新程序执行权限: $updater" ((disabled_count++)) else log_error "无法禁用更新程序: $updater" fi fi # elif [ -d "$updater" ]; then # 如果是目录,尝试禁用 # log_info "找到更新程序目录: $updater" # touch "${updater}.disabled_by_script" # log_info "已标记禁用更新程序目录: $updater" # ((disabled_count++)) fi done if [ "$disabled_count" -eq 0 ]; then log_warn "未能找到或禁用任何已知的自动更新机制。" log_warn "如果 Cursor 仍然自动更新,可能需要手动查找并禁用相关文件或设置。" else log_info "成功禁用或尝试禁用了 $disabled_count 个自动更新相关的文件/程序。" fi return 0 # 即使没找到,也认为函数执行成功 } # 新增:通用菜单选择函数 select_menu_option() { local prompt="$1" IFS='|' read -ra options <<< "$2" local default_index=${3:-0} local selected_index=$default_index local key_input local cursor_up=$'\e[A' # 更标准的 ANSI 码 local cursor_down=$'\e[B' local cursor_up_alt=$'\eOA' # 兼容应用光标模式 local cursor_down_alt=$'\eOB' local enter_key=$'\n' # 兼容管道执行场景:stdin 非 TTY 时改用 /dev/tty 读取 local input_fd=0 local input_fd_opened=0 if [ -t 0 ]; then input_fd=0 elif [ -r /dev/tty ]; then exec 3/dev/null || cat /etc/*release 2>/dev/null || cat /etc/issue" "系统版本信息" if [ -z "${CURSOR_NO_TTY_UI:-}" ]; then clear # 显示 Logo echo -e " ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ ██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ ██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ ██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ ╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ " echo -e "${BLUE}=====================================================${NC}" echo -e "${GREEN} Cursor Linux 启动与修改工具(免费) ${NC}" echo -e "${YELLOW} 关注公众号【煎饼果子卷AI】 ${NC}" echo -e "${YELLOW} 一起交流更多Cursor技巧和AI知识(脚本免费、关注公众号加群有更多技巧和大佬) ${NC}" echo -e "${BLUE}=====================================================${NC}" echo echo -e "${YELLOW}⚡ [小小广告] Cursor官网正规成品号:Pro¥65 | Pro+¥265 | Ultra¥888 独享账号| ¥488 Team绝版次数号1000次+20刀额度 | 全部7天质保 | ,WeChat:JavaRookie666 ${NC}" echo echo -e "${YELLOW}[提示]${NC} 本工具旨在修改 Cursor 以解决可能的启动问题或设备限制。" echo -e "${YELLOW}[提示]${NC} 它将优先修改 JS 文件,并可选择重置设备ID和禁用自动更新。" echo -e "${YELLOW}[提示]${NC} 如果未找到 Cursor,将尝试从 '$APPIMAGE_SEARCH_DIR' 目录安装。" echo fi # 查找 Cursor 路径 if ! find_cursor_path; then log_warn "系统中未找到现有的 Cursor 安装。" set +e select_menu_option "是否尝试从 '$APPIMAGE_SEARCH_DIR' 目录中的 AppImage 文件安装 Cursor?" "是,安装 Cursor|否,退出脚本" 0 install_choice=$? set -e if [ "$install_choice" -eq 0 ]; then if ! install_cursor_appimage; then log_error "Cursor 安装失败,请检查上面的日志。脚本将退出。" exit 1 fi # 安装成功后,重新查找路径 if ! find_cursor_path || ! find_cursor_resources; then log_error "安装后仍然无法找到 Cursor 的可执行文件或资源目录。请检查 '$INSTALL_DIR' 和 '/usr/local/bin/cursor'。脚本退出。" exit 1 fi log_info "Cursor 安装成功,继续执行修改步骤..." else log_info "用户选择不安装 Cursor,脚本退出。" exit 0 fi else # 如果找到了 Cursor,也要确保找到资源目录 if ! find_cursor_resources; then log_error "找到了 Cursor 可执行文件 ($CURSOR_PATH),但未能定位资源目录。" log_error "无法继续修改 JS 文件。请检查 Cursor 安装是否完整。脚本退出。" exit 1 fi log_info "发现已安装的 Cursor ($CURSOR_PATH),资源目录 ($CURSOR_RESOURCES)。" fi # 到这里,Cursor 应该已安装并且路径已知 # 检查并关闭Cursor进程 if ! check_and_kill_cursor; then # check_and_kill_cursor 内部会记录错误并退出,但以防万一 exit 1 fi # 执行 Cursor 初始化清理 # cursor_initialize_cleanup # 备份并处理配置文件 (机器码重置选项) if ! generate_new_config; then log_error "处理配置文件时出错,脚本中止。" # 此处可能需要考虑是否回滚JS修改(如果已执行)?目前不回滚。 exit 1 fi # 修改JS文件 log_info "正在修改 Cursor JS 文件..." if ! modify_cursor_js_files; then log_error "JS 文件修改过程中发生错误。" log_warn "配置文件可能已被修改,但 JS 文件修改失败。" log_warn "如果重启后 Cursor 行为异常或仍有问题,请检查日志并考虑手动恢复备份或重新运行脚本。" # 决定是否继续执行禁用更新?通常建议继续 # exit 1 # 或者选择退出 else log_info "JS 文件修改成功!" fi # 禁用自动更新 if ! disable_auto_update; then # disable_auto_update 内部会记录警告,不视为致命错误 log_warn "尝试禁用自动更新时遇到问题(详见日志),但脚本将继续。" fi log_info "所有修改步骤已完成!" log_info "请启动 Cursor 以应用更改。" # 显示最后的提示信息 echo echo -e "${GREEN}=====================================================${NC}" echo -e "${YELLOW} 请关注公众号【煎饼果子卷AI】获取更多技巧和交流 ${NC}" echo -e "${YELLOW}⚡ [小小广告] Cursor官网正规成品号:Pro¥65 | Pro+¥265 | Ultra¥888 独享账号| ¥488 Team绝版次数号1000次+20刀额度 | 全部7天质保 | ,WeChat:JavaRookie666 ${NC}" echo -e "${GREEN}=====================================================${NC}" echo # 记录脚本完成信息 log_info "脚本执行完成" echo "========== Cursor ID 修改工具日志结束 $(date) ==========" >> "$LOG_FILE" # 显示日志文件位置 echo log_info "详细日志已保存到: $LOG_FILE" echo "如遇问题请将此日志文件提供给开发者以协助排查" echo } # 执行主函数 main exit 0 # 确保最后返回成功状态码