智能助手网
标签聚合 两个

/tag/两个

linux.do · 2026-04-18 19:47:06+08:00 · tech

前段时间用paypal去薅GPT羊毛,因为一个paypal只能用两个team,所以想要注销后,再用paypal薅羊毛(薅了6次team,也就是反复注销了两次,这次翻车了 ,不过team都活过了一个月),这次注销了以后再登录的时候显示短暂封禁了你的部分功能,让我上传身份证正反面,上传了以后给我永久封禁了 因为这个号啥也没有就想着直接换一个号算了,所以换了一个邮箱重新注册一个号还是一样 有大佬清楚怎么解决这个问题吗,打算找个工作日和客服说说,不过听说paypal想解封很难 19 个帖子 - 9 位参与者 阅读完整话题

linux.do · 2026-04-18 17:46:34+08:00 · tech

最近半年一直在折腾一个项目——用 AI 自动生成短视频。起因是身边做小生意的朋友总问我能不能帮忙剪视频发抖音,我想这事儿应该能自动化,就开始做了。 分享一下技术方案和踩过的坑,希望对有类似想法的朋友有参考价值。 整体架构 用户上传素材 + 输入文案 ↓ TTS 语音合成(MiniMax / 火山引擎) ↓ 根据语音时长自动分配素材时间轴 ↓ 云端渲染(转场、字幕、BGM) ↓ 成品视频 后端 FastAPI + MySQL,前端 Vue 3,视频渲染走云端 API,手机端用 Capacitor 套了个壳。一个人全栈,没有团队。 几个有意思的技术点 1. TTS 选型 试了一圈,最后主力用 MiniMax speech-2.8-hd,中文多音字准确率目前最好。火山引擎作为补充,主要是剪映生态的独有音色(奶气萌娃、广告解说这种)。Azure 的中文效果一般,Fish Audio 没深入测。 踩坑:TTS 返回的音频时长和文字长度不是线性关系,语气词、停顿都会影响。批量生成时如果对时长有严格要求,需要做重试机制。 2. 素材自动分配 这块逻辑最复杂。用户上传 N 个素材(视频+图片混合),系统要自动决定每个素材展示多久、从视频的哪个位置截取。 几个关键决策: 视频从中间 70% 区域截取(跳过开头 20% 和结尾 10%),因为大部分手机拍的视频开头都是晃动的 图片展示时长根据总时长动态计算,保证每张都出现,最少 1 秒/张 批量生成时用发牌算法分配素材,保证每条视频的封面帧不同 3. 批量生成去重 做矩阵号的核心需求是"一组素材生成几十条不重复的视频"。去重策略: AI 扩写多组文案时分配不同的切入角度(预定义了 30 个角度) 每条视频的素材起始偏移不同 视频截取位置随机化 第一个 clip 强制用不同素材保证封面不同 4. 浮点精度问题 这个坑最隐蔽。视频时间轴用浮点数计算,多个 clip 拼接时 accumulated 会漂移,导致素材重叠或出现缝隙。解决方案是每次用 round 后的值更新 accumulated: cs = round(clip_start, 1) ce = round(clip_end, 1) accumulated += (ce - cs) # 不是 accumulated += clip_dur 5. 实时语音输入 加了个语音输入功能,用户对着手机说文案直接转文字。技术上是浏览器 AudioContext 采集 PCM → WebSocket 传到后端 → 转发到阿里云 paraformer-realtime-v2。 踩坑:中间结果和最终结果的拼接如果处理不好会闪烁,最后用了"快照 + 增量"的方案解决。 技术栈汇总 层 技术 前端 Vue 3 + Vite + Capacitor 后端 Python 3.13 + FastAPI + SQLAlchemy 数据库 MySQL 8 存储 阿里云 OSS + CDN AI 文案 通义千问 TTS 火山引擎 语音识别 阿里云 paraformer-realtime-v2 目前自己和几个朋友在用,做餐饮和服装的,反馈还行。有兴趣的可以体验一下: https://zj.xinghepay.com ,Android 也有 APK https://media.xinghepay.com/xinghe/app/xhzj.apk 技术上有什么想聊的欢迎评论区交流,特别是视频处理和 TTS 这块,踩的坑比较多,能聊的也多 6 个帖子 - 5 位参与者 阅读完整话题

linux.do · 2026-04-18 16:46:08+08:00 · tech

抽奖主题: GLM Coding Plan体验卡 奖品详情: [奖品]:GLM Coding Plan体验卡 * 2 活动时间: 开始时间:[此帖发出开始] 截止时间: Sat, Apr 18, 2026 10:00 PM CST 参与方式: 在本帖下回复任意内容 抽奖规则: 每位用户仅允许参与一次。 将使用 LINUX DO 抽奖工具 在所有回复中随机抽取中奖者。 注意事项: 本活动将在活动截止时间后关闭回帖,以确保公正性。 中奖者将在活动结束后在本帖公布,并通过论坛站内信由发起人通知领奖方式。 所有规则及抽奖结果由 @feilong 及论坛 管理团队 最终解释。 发起人承诺: 作为本次抽奖的发起人 @feilong ,我承诺本话题的抽奖活动严格遵守 LINUX DO 社区抽奖规则 。因违反上述规定引发的公平性争议或其他问题,均由我独立承担相应的道德与法律责任。 期待您的积极参与,祝您好运!如有任何疑问,欢迎随时联系 @feilong 或论坛 管理团队 。 20 个帖子 - 19 位参与者 阅读完整话题

linux.do · 2026-04-17 23:14:59+08:00 · tech

gpt team位置2个,无质保 活动时间: 开始时间:发帖时间 截止时间: Sat, Apr 18, 2026 12:00 AM CST 兑奖时间: Sat, Apr 18, 2026 12:00 AM CST ~ Sat, Apr 18, 2026 1:00 AM CST ,期间中奖人私信回复邮箱视为有效兑奖,过时视为放弃 参与方式: 在本帖下回复任意内容 抽奖规则: 每位用户仅允许参与一次。 使用 官方抽奖工具 随机抽取中奖者。 注意事项: 本活动将在活动截止时间后关闭回帖,以确保公正性。 中奖者将在活动结束后12小时内在本帖公布,并通过私信通知领奖方式。 所有规则及抽奖结果由活动发起人 @dovelf 和论坛 管理团队 最终解释。 期待您的积极参与,祝您好运!如有任何疑问,欢迎随时联系抽奖发起人。 88 个帖子 - 86 位参与者 阅读完整话题

linux.do · 2026-04-17 20:22:10+08:00 · tech

这个投票我已经在某两个主机论坛上发布过了, 得出了不一样的结果. 想着 Linux.do 也是一个典型的支持商家入驻的垂直领域论坛, 所以也来这里调查一下各位佬友的看法. 不管商家在服务条款规定了怎么样的无条件解约条款, 还是客户在商家的地盘里面做了什么不得了的事情, 所以到底可不可以呢? 这是个原则上的可以和不可以问题, 不需要任何前置理由, 直接选择你的第一反应就好. 商家可以因为客户给出差评而终止服务合同吗? 商家可以因为客户给出差评而终止服务合同吗? 可以 不可以 点击以查看投票。 12 个帖子 - 7 位参与者 阅读完整话题

linux.do · 2026-04-17 20:15:10+08:00 · tech

这个帖子要给佬友们道两个歉 第一: 之前的抽奖帖子被举报没了,所以抽中奖的佬友们该怎么发奖完全没有思路 PS:所有帖子都没了,确定没有被针对? 第二个歉:主站 https://api.chy本尊.top不能用了 ,具体原因正在调查 不过不用慌,临时域名还是可用 https://chybenzun666-new-api.hf.space/ 为了表达歉意,公益站注册已经打开 34 个帖子 - 31 位参与者 阅读完整话题

linux.do · 2026-04-17 19:15:40+08:00 · tech

楼主有两个5x账号,深感切换不便,便写了个脚本,可能会有bug,请自行用claude/codex修复~。 需要提前运行: pip install rich 进行rich库安装 #!/usr/bin/env python3 from __future__ import annotations import json import os import secrets import shlex import shutil import subprocess import sys import hashlib from datetime import datetime from pathlib import Path from typing import Any try: import pwd # type: ignore except ImportError: # pragma: no cover - Windows pwd = None # type: ignore try: from rich.console import Console from rich.panel import Panel from rich.prompt import Confirm, Prompt from rich.table import Table from rich.text import Text except ImportError: print("缺少依赖 rich,请先执行: pip install rich", file=sys.stderr) sys.exit(1) console = Console() HOME = Path.home() ROOT = Path(os.environ.get("CLAUDE_SWITCHER_HOME", HOME / ".claude-switcher-direct")) SLOTS_HOME = ROOT / "slots" AUTO_BACKUPS_HOME = ROOT / "auto-backups" STATE_FILE = ROOT / "state.json" LIVE_MODERN_CONFIG = HOME / ".claude.json" LIVE_LEGACY_CONFIG = HOME / ".claude" / ".config.json" LIVE_CREDENTIALS = HOME / ".claude" / ".credentials.json" RESERVED_COMMANDS = { "help", "--help", "-h", "tui", "add-account", "add", "doctor", "check", "normalize-live", "normalize", "list", "ls", "save", "capture", "switch", "use", "login", "logout", "launch", "run", "current", "whoami", "paths", "env", "remove", "rm", } def effective_platform() -> str: forced = os.environ.get("CLAUDE_SWITCHER_FORCE_PLATFORM") if forced: return forced return sys.platform def is_macos() -> bool: return effective_platform() == "darwin" def env_truthy(name: str) -> bool: value = os.environ.get(name) if value is None: return False return value.strip().lower() in {"1", "true", "yes", "on"} def ensure_dir(path: Path) -> None: path.mkdir(parents=True, exist_ok=True) def read_json(path: Path, fallback: Any = None) -> Any: try: return json.loads(path.read_text(encoding="utf-8")) except Exception: return fallback def write_json(path: Path, data: Any) -> None: ensure_dir(path.parent) path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") def write_bytes(path: Path, data: bytes, *, chmod_600: bool = False) -> None: ensure_dir(path.parent) path.write_bytes(data) if chmod_600 and os.name != "nt": try: path.chmod(0o600) except Exception: pass def timestamp_slug() -> str: return datetime.now().strftime("%Y%m%d_%H%M%S_%f") def sanitize_name(name: str) -> str: invalid = '<>:"/\\|?*' out: list[str] = [] for ch in name.strip(): if ord(ch) < 32 or ch in invalid: out.append("-") elif ch.isspace(): out.append("-") else: out.append(ch) text = "".join(out) while "--" in text: text = text.replace("--", "-") return text.strip("-") def require_name(name: str | None, what: str = "名称") -> str: value = (name or "").strip() if not value: fail(f"缺少{what}。") return value def load_state() -> dict[str, Any]: state = read_json(STATE_FILE, None) if isinstance(state, dict) and isinstance(state.get("slots"), dict): state.setdefault("version", 1) state.setdefault("lastApplied", None) state.setdefault("accountUserIDs", {}) return state return {"version": 1, "lastApplied": None, "slots": {}, "accountUserIDs": {}} def save_state(state: dict[str, Any]) -> None: write_json(STATE_FILE, state) def oauth_file_suffix() -> str: if os.environ.get("CLAUDE_CODE_CUSTOM_OAUTH_URL"): return "-custom-oauth" if os.environ.get("USER_TYPE") == "ant": if env_truthy("USE_LOCAL_OAUTH"): return "-local-oauth" if env_truthy("USE_STAGING_OAUTH"): return "-staging-oauth" return "" def get_claude_config_home_dir() -> Path: custom = os.environ.get("CLAUDE_CONFIG_DIR") if custom: return Path(custom).expanduser() return HOME / ".claude" def get_macos_keychain_service_name() -> str: config_dir = str(get_claude_config_home_dir()) is_default_dir = "CLAUDE_CONFIG_DIR" not in os.environ dir_hash = "" if is_default_dir else "-" + hashlib.sha256(config_dir.encode("utf-8")).hexdigest()[:8] return f"Claude Code{oauth_file_suffix()}-credentials{dir_hash}" def get_macos_keychain_username() -> str: if os.environ.get("USER"): return os.environ["USER"] if pwd is not None: try: return pwd.getpwuid(os.getuid()).pw_name except Exception: pass return "claude-code-user" def get_security_bin() -> str: return os.environ.get("CLAUDE_SWITCHER_SECURITY_BIN", "security") def read_macos_keychain_json() -> dict[str, Any] | None: if not is_macos(): return None try: result = subprocess.run( [ get_security_bin(), "find-generic-password", "-a", get_macos_keychain_username(), "-w", "-s", get_macos_keychain_service_name(), ], capture_output=True, text=True, check=False, ) except FileNotFoundError: return None except Exception: return None if result.returncode != 0 or not result.stdout: return None try: return json.loads(result.stdout.strip()) except Exception: return None def write_macos_keychain_json(data: dict[str, Any]) -> bool: if not is_macos(): return False try: payload = json.dumps(data, ensure_ascii=False, indent=2) hex_value = payload.encode("utf-8").hex() result = subprocess.run( [ get_security_bin(), "add-generic-password", "-U", "-a", get_macos_keychain_username(), "-s", get_macos_keychain_service_name(), "-X", hex_value, ], capture_output=True, text=True, check=False, ) return result.returncode == 0 except Exception: return False def read_live_credentials_json() -> tuple[dict[str, Any] | None, str]: if is_macos(): keychain_data = read_macos_keychain_json() if isinstance(keychain_data, dict): return keychain_data, "keychain" file_data = read_json(LIVE_CREDENTIALS, None) if isinstance(file_data, dict): return file_data, "file" return None, "missing" def generate_user_id() -> str: return secrets.token_hex(32) def short_id(value: str | None, length: int = 12) -> str: if not value: return "-" if len(value) <= length: return value return f"{value[:length]}..." def account_key(email: str | None, account_uuid: str | None) -> str | None: if account_uuid: return f"account_uuid:{account_uuid}" if email: return f"email:{email.strip().lower()}" return None def remember_account_user_id( state: dict[str, Any], *, user_id: str | None, email: str | None, account_uuid: str | None, ) -> None: key = account_key(email, account_uuid) if not key or not user_id: return state.setdefault("accountUserIDs", {}) state["accountUserIDs"][key] = user_id def get_account_bound_user_id( state: dict[str, Any], *, email: str | None, account_uuid: str | None, ) -> str | None: key = account_key(email, account_uuid) if not key: return None value = (state.get("accountUserIDs") or {}).get(key) return value if isinstance(value, str) and value else None def get_saved_user_ids(state: dict[str, Any], *, exclude_name: str | None = None) -> set[str]: found: set[str] = set() for slot_name, slot in state.get("slots", {}).items(): if exclude_name and slot_name == exclude_name: continue user_id = slot.get("userID") if isinstance(user_id, str) and user_id: found.add(user_id) continue meta = read_json(slot_files(Path(slot["dir"]))["meta"], {}) or {} meta_user_id = meta.get("userID") if isinstance(meta_user_id, str) and meta_user_id: found.add(meta_user_id) return found def choose_slot_user_id( state: dict[str, Any], slot_name: str, preferred: str | None = None, *, email: str | None = None, account_uuid: str | None = None, ) -> str: slot = state.get("slots", {}).get(slot_name) or {} email = email or slot.get("email") account_uuid = account_uuid or slot.get("accountUuid") requested_key = account_key(email, account_uuid) slot_key = account_key(slot.get("email"), slot.get("accountUuid")) bound = get_account_bound_user_id(state, email=email, account_uuid=account_uuid) if bound: return bound reuse_slot_specific_id = not requested_key or not slot_key or requested_key == slot_key existing = slot.get("userID") if reuse_slot_specific_id and isinstance(existing, str) and existing: return existing meta = read_json(slot_files(slot_dir(slot_name))["meta"], {}) or {} bound = get_account_bound_user_id( state, email=meta.get("email"), account_uuid=meta.get("accountUuid"), ) if bound: return bound meta_user_id = meta.get("userID") meta_key = account_key(meta.get("email"), meta.get("accountUuid")) if (reuse_slot_specific_id or not meta_key or meta_key == requested_key) and isinstance(meta_user_id, str) and meta_user_id: return meta_user_id used = get_saved_user_ids(state, exclude_name=slot_name) if isinstance(preferred, str) and preferred and preferred not in used: return preferred while True: candidate = generate_user_id() if candidate not in used: return candidate def apply_user_id_to_snapshot(directory: Path, user_id: str) -> None: files = slot_files(directory) config = read_json(files["config"], None) if isinstance(config, dict): config["userID"] = user_id write_json(files["config"], config) meta = read_json(files["meta"], {}) or {} meta["userID"] = user_id write_json(files["meta"], meta) def apply_user_id_to_live(user_id: str) -> None: paths = live_paths() config = read_json(paths["active_config"], None) if not isinstance(config, dict): return config["userID"] = user_id payload = json.dumps(config, ensure_ascii=False, indent=2).encode("utf-8") write_bytes(LIVE_MODERN_CONFIG, payload) write_bytes(LIVE_LEGACY_CONFIG, payload) def slot_dir(name: str) -> Path: safe = sanitize_name(name) if not safe: fail("slot 名称非法。") return (SLOTS_HOME / safe).resolve() def slot_files(directory: Path) -> dict[str, Path]: return { "config": directory / "global_config.json", "credentials": directory / "credentials.json", "macos_keychain": directory / "macos_keychain_credentials.json", "meta": directory / "meta.json", } def live_paths() -> dict[str, Path]: active_config = LIVE_LEGACY_CONFIG if LIVE_LEGACY_CONFIG.exists() else LIVE_MODERN_CONFIG return { "modern_config": LIVE_MODERN_CONFIG, "legacy_config": LIVE_LEGACY_CONFIG, "active_config": active_config, "credentials": LIVE_CREDENTIALS, } def detect_claude_command() -> str: return os.environ.get("CLAUDE_BIN") or ("claude.cmd" if os.name == "nt" else "claude") def get_installed_claude_info() -> dict[str, Any]: command = detect_claude_command() resolved = shutil.which(command) info: dict[str, Any] = { "command": command, "resolved": resolved, "package_json": None, "version": None, } if not resolved: return info resolved_path = Path(resolved) candidates = [] if resolved_path.name.lower().endswith(".cmd") or resolved_path.name.lower().endswith(".ps1"): candidates.append(resolved_path.parent / "node_modules" / "@anthropic-ai" / "claude-code" / "package.json") candidates.append(resolved_path.parent / "node_modules" / "@anthropic-ai" / "claude-code" / "package.json") for candidate in candidates: if candidate.exists(): info["package_json"] = str(candidate) pkg = read_json(candidate, {}) or {} if isinstance(pkg, dict): info["version"] = pkg.get("version") break return info def run_claude(args: list[str]) -> int: claude_bin = detect_claude_command() command_preview = f"{claude_bin} {' '.join(shlex.quote(a) for a in args)}".strip() console.print( Panel( Text.from_markup( f"[bold cyan]启动 Claude[/]\n" f"命令: [magenta]{command_preview}[/]\n" f"当前 live 文件: [yellow]{live_paths()['active_config']}[/]" ), title="Launch", border_style="cyan", ) ) try: if os.name == "nt": cmdline = subprocess.list2cmdline([claude_bin, *args]) result = subprocess.run(cmdline, shell=True) else: result = subprocess.run([claude_bin, *args]) return int(result.returncode) except FileNotFoundError: fail("启动 Claude 失败:未找到 claude 命令。可检查 PATH,或设置 CLAUDE_BIN。") except Exception as exc: fail(f"启动 Claude 失败:{exc}") return 1 def read_live_status() -> dict[str, Any]: paths = live_paths() config = read_json(paths["active_config"], {}) or {} credentials, credentials_source = read_live_credentials_json() credentials = credentials or {} oauth = credentials.get("claudeAiOauth") or {} return { "active_config_path": str(paths["active_config"]), "modern_config_exists": paths["modern_config"].exists(), "legacy_config_exists": paths["legacy_config"].exists(), "credentials_exists": paths["credentials"].exists(), "credentials_source": credentials_source, "macos_keychain_service": get_macos_keychain_service_name() if is_macos() else None, "macos_keychain_present": credentials_source == "keychain", "user_id": config.get("userID") or None, "email": (((config.get("oauthAccount") or {}).get("emailAddress")) or None), "account_uuid": (((config.get("oauthAccount") or {}).get("accountUuid")) or None), "organization_uuid": (((config.get("oauthAccount") or {}).get("organizationUuid")) or None), "has_access_token": bool(oauth.get("accessToken")), "has_refresh_token": bool(oauth.get("refreshToken")), "expires_at": oauth.get("expiresAt"), "subscription_type": oauth.get("subscriptionType"), "rate_limit_tier": oauth.get("rateLimitTier"), } def read_slot_status(name: str) -> dict[str, Any]: files = slot_files(slot_dir(name)) meta = read_json(files["meta"], {}) or {} config = read_json(files["config"], {}) or {} credentials = read_json(files["credentials"], {}) or {} oauth = credentials.get("claudeAiOauth") or {} return { "name": name, "dir": str(files["meta"].parent), "saved_at": meta.get("savedAt"), "kind": meta.get("kind", "manual"), "user_id": meta.get("userID") or config.get("userID") or None, "email": meta.get("email") or (((config.get("oauthAccount") or {}).get("emailAddress")) or None), "account_uuid": meta.get("accountUuid") or (((config.get("oauthAccount") or {}).get("accountUuid")) or None), "organization_uuid": (((config.get("oauthAccount") or {}).get("organizationUuid")) or None), "has_config": files["config"].exists(), "has_credentials": files["credentials"].exists(), "has_macos_keychain_snapshot": files["macos_keychain"].exists(), "has_access_token": bool(oauth.get("accessToken")), "has_refresh_token": bool(oauth.get("refreshToken")), "expires_at": oauth.get("expiresAt"), "subscription_type": oauth.get("subscriptionType"), "rate_limit_tier": oauth.get("rateLimitTier"), "meta": meta, } def format_time(value: Any) -> str: if not value: return "-" try: return datetime.fromtimestamp(float(value) / 1000.0).strftime("%Y-%m-%d %H:%M:%S") except Exception: return str(value) def fail(message: str) -> None: console.print(f"[bold red][claude-switcher][/bold red] {message}") raise SystemExit(1) def ok(message: str) -> None: console.print(f"[bold green][OK][/bold green] {message}") def note(message: str) -> None: console.print(f"[bold yellow][INFO][/bold yellow] {message}") def save_snapshot_from_live(target_dir: Path, name: str, kind: str) -> dict[str, Any]: ensure_dir(target_dir) live = live_paths() copied_any = False if live["active_config"].exists(): write_bytes(slot_files(target_dir)["config"], live["active_config"].read_bytes()) copied_any = True credentials_json, credentials_source = read_live_credentials_json() if isinstance(credentials_json, dict): payload = json.dumps(credentials_json, ensure_ascii=False, indent=2).encode("utf-8") write_bytes(slot_files(target_dir)["credentials"], payload, chmod_600=True) if is_macos(): write_bytes(slot_files(target_dir)["macos_keychain"], payload, chmod_600=True) copied_any = True elif live["credentials"].exists(): write_bytes(slot_files(target_dir)["credentials"], live["credentials"].read_bytes(), chmod_600=True) copied_any = True if not copied_any: fail("当前 live 文件里没有可备份内容(未找到配置或凭证文件)。") status = read_live_status() meta = { "name": name, "kind": kind, "savedAt": datetime.now().isoformat(timespec="seconds"), "userID": status["user_id"], "email": status["email"], "accountUuid": status["account_uuid"], "activeConfigPath": status["active_config_path"], "credentialsSource": credentials_source, "modernConfigExists": status["modern_config_exists"], "legacyConfigExists": status["legacy_config_exists"], "credentialsExists": status["credentials_exists"], } write_json(slot_files(target_dir)["meta"], meta) return meta def create_auto_backup() -> dict[str, Any] | None: live = live_paths() if not live["active_config"].exists() and not live["credentials"].exists(): return None name = f"auto_{timestamp_slug()}" directory = AUTO_BACKUPS_HOME / name meta = save_snapshot_from_live(directory, name, "auto") meta["dir"] = str(directory) return meta def save_live_to_slot(name: str | None) -> dict[str, Any]: slot_name = require_name(name, "slot 名称") state = load_state() directory = slot_dir(slot_name) meta = save_snapshot_from_live(directory, slot_name, "manual") slot_user_id = choose_slot_user_id( state, slot_name, preferred=meta.get("userID"), email=meta.get("email"), account_uuid=meta.get("accountUuid"), ) apply_user_id_to_snapshot(directory, slot_user_id) apply_user_id_to_live(slot_user_id) meta = read_json(slot_files(directory)["meta"], {}) or meta state["slots"][slot_name] = { "name": slot_name, "dir": str(directory), "savedAt": meta["savedAt"], "userID": slot_user_id, "email": meta.get("email"), "accountUuid": meta.get("accountUuid"), } remember_account_user_id( state, user_id=slot_user_id, email=meta.get("email"), account_uuid=meta.get("accountUuid"), ) state["lastApplied"] = slot_name save_state(state) ok(f"已把当前 live 文件保存到 slot: {slot_name}") note(f"目录: {directory}") note(f"userID: {slot_user_id}") note("当前 live .claude.json 也已同步为这个 slot 的 userID") return meta def ensure_slot_exists(state: dict[str, Any], name: str) -> dict[str, Any]: slot = state["slots"].get(name) if not slot: fail(f"找不到 slot: {name}") return slot def restore_slot_to_live(name: str | None, *, backup_current: bool = True) -> dict[str, Any] | None: slot_name = require_name(name, "slot 名称") state = load_state() slot = ensure_slot_exists(state, slot_name) directory = Path(slot["dir"]) files = slot_files(directory) slot_status = read_slot_status(slot_name) if not files["config"].exists() and not files["credentials"].exists(): fail(f"slot {slot_name} 没有可恢复的文件。") auto_meta = create_auto_backup() if backup_current else None if files["config"].exists(): slot_user_id = choose_slot_user_id( state, slot_name, preferred=slot_status["user_id"], email=slot_status["email"], account_uuid=slot_status["account_uuid"], ) config_data = read_json(files["config"], None) if isinstance(config_data, dict): config_data["userID"] = slot_user_id write_json(files["config"], config_data) apply_user_id_to_snapshot(directory, slot_user_id) config_bytes = json.dumps(config_data, ensure_ascii=False, indent=2).encode("utf-8") else: config_bytes = files["config"].read_bytes() slot_user_id = slot.get("userID") or slot_status["user_id"] # 为了兼容 Claude 源码里 legacy 优先逻辑,恢复时同步写到两个路径 write_bytes(LIVE_MODERN_CONFIG, config_bytes) write_bytes(LIVE_LEGACY_CONFIG, config_bytes) if slot_user_id: slot["userID"] = slot_user_id if files["credentials"].exists(): write_bytes(LIVE_CREDENTIALS, files["credentials"].read_bytes(), chmod_600=True) if is_macos(): macos_source_file = files["macos_keychain"] if files["macos_keychain"].exists() else files["credentials"] macos_payload = read_json(macos_source_file, None) if isinstance(macos_payload, dict): if write_macos_keychain_json(macos_payload): note(f"已恢复 macOS Keychain: {get_macos_keychain_service_name()}") else: note("警告:macOS Keychain 恢复失败,当前将依赖 .credentials.json fallback") state["lastApplied"] = slot_name slot["email"] = slot_status["email"] slot["accountUuid"] = slot_status["account_uuid"] remember_account_user_id( state, user_id=slot.get("userID"), email=slot_status["email"], account_uuid=slot_status["account_uuid"], ) save_state(state) ok(f"已恢复 slot 到 live 文件: {slot_name}") if auto_meta: note(f"切换前自动备份: {auto_meta['dir']}") note(f"live config: {LIVE_MODERN_CONFIG} + {LIVE_LEGACY_CONFIG}") note(f"live credentials: {LIVE_CREDENTIALS}") if slot.get("userID"): note(f"已写入 slot 专属 userID: {slot['userID']}") return auto_meta def remove_slot(name: str | None) -> None: slot_name = require_name(name, "slot 名称") state = load_state() slot = ensure_slot_exists(state, slot_name) directory = Path(slot["dir"]) if directory.exists(): shutil.rmtree(directory) del state["slots"][slot_name] if state.get("lastApplied") == slot_name: state["lastApplied"] = None save_state(state) ok(f"已删除 slot: {slot_name}") def show_current() -> None: state = load_state() live = read_live_status() console.print(f"[bold cyan]lastApplied:[/] {state.get('lastApplied') or '-'}") console.print(f"[bold cyan]live email:[/] {live['email'] or '-'}") console.print(f"[bold cyan]live userID:[/] {live['user_id'] or '-'}") console.print(f"[bold cyan]active config:[/] {live['active_config_path']}") def show_whoami(name: str | None = None) -> None: if name: status = read_slot_status(name) title = f"Slot: {name}" else: status = read_live_status() title = "Live Files" table = Table(title=title, show_header=False, box=None) table.add_column("k", style="cyan", no_wrap=True) table.add_column("v") if name: table.add_row("dir", status["dir"]) table.add_row("savedAt", status["saved_at"] or "-") else: table.add_row("activeConfig", status["active_config_path"]) table.add_row("modernConfigExists", "yes" if status["modern_config_exists"] else "no") table.add_row("legacyConfigExists", "yes" if status["legacy_config_exists"] else "no") table.add_row("credentialsExists", "yes" if status["credentials_exists"] else "no") table.add_row("userID", status["user_id"] or "-") table.add_row("email", status["email"] or "-") table.add_row("accountUuid", status["account_uuid"] or "-") table.add_row("organizationUuid", status["organization_uuid"] or "-") table.add_row("subscriptionType", status["subscription_type"] or "-") table.add_row("rateLimitTier", status["rate_limit_tier"] or "-") table.add_row("hasAccessToken", "yes" if status["has_access_token"] else "no") table.add_row("hasRefreshToken", "yes" if status["has_refresh_token"] else "no") table.add_row("expiresAt", format_time(status["expires_at"])) console.print(table) def show_paths(name: str | None = None) -> None: table = Table(show_header=False, box=None, title="Paths") table.add_column("k", style="cyan", no_wrap=True) table.add_column("v") table.add_row("storageRoot", str(ROOT)) table.add_row("slotsRoot", str(SLOTS_HOME)) table.add_row("autoBackupsRoot", str(AUTO_BACKUPS_HOME)) table.add_row("liveModernConfig", str(LIVE_MODERN_CONFIG)) table.add_row("liveLegacyConfig", str(LIVE_LEGACY_CONFIG)) table.add_row("liveCredentials", str(LIVE_CREDENTIALS)) if is_macos(): table.add_row("macOSKeychainService", get_macos_keychain_service_name()) if name: table.add_row("slotDir", str(slot_dir(name))) files = slot_files(slot_dir(name)) table.add_row("slotConfig", str(files["config"])) table.add_row("slotCredentials", str(files["credentials"])) table.add_row("slotMacKeychain", str(files["macos_keychain"])) table.add_row("slotMeta", str(files["meta"])) console.print(table) def print_env() -> None: console.print("[yellow]这个版本不依赖 CLAUDE_CONFIG_DIR。[/]") console.print("[yellow]它会直接修改官方 live 文件:[/]") console.print(str(LIVE_MODERN_CONFIG)) console.print(str(LIVE_LEGACY_CONFIG)) console.print(str(LIVE_CREDENTIALS)) def normalize_live_config(*, backup_current: bool = True) -> dict[str, Any] | None: live = live_paths() config = read_json(live["active_config"], None) if not isinstance(config, dict): fail("当前 live config 不存在或无法解析,无法 normalize。") auto_meta = create_auto_backup() if backup_current else None payload = json.dumps(config, ensure_ascii=False, indent=2).encode("utf-8") write_bytes(LIVE_MODERN_CONFIG, payload) write_bytes(LIVE_LEGACY_CONFIG, payload) ok("已完成 live config normalize") if auto_meta: note(f"normalize 前自动备份: {auto_meta['dir']}") note(f"已同步: {LIVE_MODERN_CONFIG}") note(f"已同步: {LIVE_LEGACY_CONFIG}") return auto_meta def collect_doctor_data() -> dict[str, Any]: state = load_state() live = read_live_status() install = get_installed_claude_info() findings: list[tuple[str, str]] = [] warnings: list[str] = [] ok_items: list[str] = [] if install["resolved"]: ok_items.append(f"已检测到 Claude 命令: {install['resolved']}") else: findings.append(("error", "未在 PATH 中找到 claude 命令")) if install["version"]: ok_items.append(f"本机 Claude Code 版本: {install['version']}") else: warnings.append("未能解析已安装 Claude Code 版本") if live["modern_config_exists"] or live["legacy_config_exists"]: ok_items.append(f"检测到 live config: {live['active_config_path']}") else: findings.append(("error", "未找到 live config(.claude.json / .claude/.config.json)")) if live["credentials_exists"]: ok_items.append("检测到 live credentials") else: warnings.append("未找到 live .credentials.json") if is_macos(): if live["macos_keychain_present"]: ok_items.append(f"检测到 macOS Keychain 凭证: {live['macos_keychain_service']}") else: warnings.append("macOS 未检测到 Keychain OAuth 凭证,将依赖 .credentials.json fallback") if live["user_id"]: ok_items.append("live userID 存在") else: findings.append(("error", "live config 缺少 userID")) slots = state.get("slots", {}) account_to_user_ids: dict[str, set[str]] = {} user_id_to_accounts: dict[str, set[str]] = {} for slot_name, slot in sorted(slots.items()): status = read_slot_status(slot_name) files = slot_files(Path(slot["dir"])) meta = read_json(files["meta"], {}) or {} config = read_json(files["config"], {}) or {} if not files["config"].exists(): findings.append(("error", f"slot {slot_name} 缺少 global_config.json")) if not files["credentials"].exists(): warnings.append(f"slot {slot_name} 缺少 credentials.json") if is_macos() and not files["macos_keychain"].exists(): warnings.append(f"slot {slot_name} 缺少 macos_keychain_credentials.json") if not files["meta"].exists(): warnings.append(f"slot {slot_name} 缺少 meta.json") state_user_id = slot.get("userID") meta_user_id = meta.get("userID") config_user_id = config.get("userID") if isinstance(config, dict) else None if state_user_id and meta_user_id and state_user_id != meta_user_id: findings.append(("error", f"slot {slot_name} 的 state.userID 与 meta.userID 不一致")) if state_user_id and config_user_id and state_user_id != config_user_id: findings.append(("error", f"slot {slot_name} 的 state.userID 与 config.userID 不一致")) if not status["user_id"]: findings.append(("error", f"slot {slot_name} 缺少 userID")) acc_key = account_key(status["email"], status["account_uuid"]) if acc_key and status["user_id"]: account_to_user_ids.setdefault(acc_key, set()).add(status["user_id"]) user_id_to_accounts.setdefault(status["user_id"], set()).add(acc_key) for acc_key, user_ids in sorted(account_to_user_ids.items()): if len(user_ids) > 1: findings.append(("error", f"同一账号 {acc_key} 绑定了多个 userID: {', '.join(sorted(user_ids))}")) for user_id, account_keys in sorted(user_id_to_accounts.items()): if len(account_keys) > 1: findings.append( ( "error", f"userID {user_id} 被多个账号共用: {', '.join(sorted(account_keys))}", ) ) last_applied = state.get("lastApplied") if last_applied: if last_applied not in slots: findings.append(("error", f"lastApplied 指向不存在的 slot: {last_applied}")) else: last_status = read_slot_status(last_applied) if live["user_id"] and last_status["user_id"] and live["user_id"] != last_status["user_id"]: warnings.append( f"当前 live userID 与 lastApplied({last_applied}) 不一致,说明 live 状态可能被外部 login/logout 改过" ) return { "state": state, "live": live, "install": install, "findings": findings, "warnings": warnings, "ok_items": ok_items, } def run_doctor() -> bool: data = collect_doctor_data() install = data["install"] live = data["live"] findings = data["findings"] warnings = data["warnings"] ok_items = data["ok_items"] state = data["state"] summary = Table(show_header=False, box=None, title="Doctor Summary") summary.add_column("k", style="cyan", no_wrap=True) summary.add_column("v") summary.add_row("Claude command", install["resolved"] or "-") summary.add_row("Claude version", install["version"] or "-") summary.add_row("lastApplied", state.get("lastApplied") or "-") summary.add_row("live email", live["email"] or "-") summary.add_row("live accountUuid", live["account_uuid"] or "-") summary.add_row("live userID", live["user_id"] or "-") summary.add_row("credentials source", live["credentials_source"] or "-") if is_macos(): summary.add_row("macOS keychain service", live["macos_keychain_service"] or "-") summary.add_row("slot count", str(len(state.get("slots", {})))) console.print(summary) if ok_items: ok_table = Table(title="Checks OK", show_header=False, box=None) ok_table.add_column("v", style="green") for item in ok_items: ok_table.add_row(f"[OK] {item}") console.print(ok_table) if warnings: warn_table = Table(title="Warnings", show_header=False, box=None) warn_table.add_column("v", style="yellow") for item in warnings: warn_table.add_row(f"[WARN] {item}") console.print(warn_table) if findings: err_table = Table(title="Problems", show_header=False, box=None) err_table.add_column("severity", style="red", no_wrap=True) err_table.add_column("message") for severity, message in findings: err_table.add_row(severity.upper(), message) console.print(err_table) console.print("[bold red]Doctor 发现问题,请先修复再大规模使用。[/]") return False console.print("[bold green]Doctor 检查通过:当前配置和已保存 slot 没发现硬冲突。[/]") return True def list_slots() -> None: state = load_state() console.print(render_header(state)) console.print(render_live_panel(state)) console.print(render_slots_table(state)) console.print(render_auto_backups_table()) def render_header(state: dict[str, Any]) -> Panel: body = Text() body.append("模式: ", style="bold cyan") body.append("复制文件备份 + 直接修改 live 文件\n", style="bold green") body.append("lastApplied: ", style="bold cyan") body.append(f"{state.get('lastApplied') or '-'}\n", style="white") body.append("Claude 命令: ", style="bold cyan") body.append(detect_claude_command(), style="magenta") return Panel(body, title="Claude Switcher Direct", border_style="cyan") def render_live_panel(state: dict[str, Any]) -> Panel: live = read_live_status() body = Text() body.append("当前 live 邮箱: ", style="bold cyan") body.append(f"{live['email'] or '-'}\n", style="white") body.append("当前 live userID: ", style="bold cyan") body.append(f"{short_id(live['user_id'], 20)}\n", style="white") body.append("Active config: ", style="bold cyan") body.append(f"{live['active_config_path']}\n", style="white") body.append("Access/Refresh: ", style="bold cyan") body.append( f"{'yes' if live['has_access_token'] else 'no'} / {'yes' if live['has_refresh_token'] else 'no'}\n", style="white", ) body.append("过期时间: ", style="bold cyan") body.append(format_time(live["expires_at"]), style="white") return Panel(body, title="Live Files", border_style="green") def render_slots_table(state: dict[str, Any]) -> Table: table = Table(title="Saved Slots", expand=True) table.add_column("#", style="dim", width=4, justify="right") table.add_column("名称", style="bold") table.add_column("userID", width=16) table.add_column("邮箱") table.add_column("Access", width=8, justify="center") table.add_column("Refresh", width=8, justify="center") table.add_column("保存时间", width=20) table.add_column("目录", overflow="fold") names = sorted(state["slots"]) if not names: table.add_row("-", "还没有 slot", "-", "-", "-", "-", "-", "-") return table for idx, name in enumerate(names, 1): status = read_slot_status(name) table.add_row( str(idx), name, short_id(status["user_id"], 14), status["email"] or "-", "[green]yes[/]" if status["has_access_token"] else "[red]no[/]", "[green]yes[/]" if status["has_refresh_token"] else "[red]no[/]", str(status["saved_at"] or "-"), status["dir"], ) return table def render_slot_picker_table(state: dict[str, Any], title: str = "请选择账号") -> Table: table = Table(title=title, expand=True) table.add_column("序号", style="dim", width=6, justify="right") table.add_column("名称", style="bold") table.add_column("邮箱") table.add_column("userID", width=16) table.add_column("保存时间", width=20) names = sorted(state["slots"]) if not names: table.add_row("-", "还没有 slot", "-", "-", "-") return table for idx, name in enumerate(names, 1): status = read_slot_status(name) table.add_row( str(idx), name, status["email"] or "-", short_id(status["user_id"], 14), str(status["saved_at"] or "-"), ) return table def render_auto_backups_table(limit: int = 5) -> Table: table = Table(title=f"Recent Auto Backups (latest {limit})", expand=True) table.add_column("名称", style="bold") table.add_column("邮箱") table.add_column("保存时间") table.add_column("目录", overflow="fold") if not AUTO_BACKUPS_HOME.exists(): table.add_row("-", "-", "-", "-") return table backup_dirs = sorted( [p for p in AUTO_BACKUPS_HOME.iterdir() if p.is_dir()], key=lambda p: p.name, reverse=True, )[:limit] if not backup_dirs: table.add_row("-", "-", "-", "-") return table for directory in backup_dirs: meta = read_json(directory / "meta.json", {}) or {} table.add_row( directory.name, meta.get("email") or "-", meta.get("savedAt") or "-", str(directory), ) return table def pause() -> None: console.input("\n[dim]按 Enter 继续...[/]") def pick_slot_name_interactive(prompt_text: str) -> str: state = load_state() names = sorted(state["slots"]) if not names: fail("还没有 slot。") console.print(render_slot_picker_table(state)) raw = Prompt.ask(prompt_text).strip() if raw.isdigit(): idx = int(raw) if 1 <= idx <= len(names): return names[idx - 1] fail(f"序号超出范围: {raw}") return require_name(raw, "slot 名称或序号") def select_slot(state: dict[str, Any], prompt_text: str) -> str: names = sorted(state["slots"]) if not names: fail("还没有 slot。") console.print(render_slot_picker_table(state)) raw = Prompt.ask(prompt_text).strip() if raw.isdigit(): idx = int(raw) if 1 <= idx <= len(names): return names[idx - 1] fail(f"序号超出范围: {raw}") return require_name(raw, "slot 名称") def resolve_slot_input(name_or_index: str | None, *, prompt_text: str) -> str: state = load_state() names = sorted(state["slots"]) if not names: fail("还没有 slot。") raw = (name_or_index or "").strip() if not raw: return pick_slot_name_interactive(prompt_text) if raw.isdigit(): idx = int(raw) if 1 <= idx <= len(names): return names[idx - 1] fail(f"序号超出范围: {raw}") if raw not in state["slots"]: fail(f"找不到 slot: {raw}") return raw def tui_save_slot() -> None: name = Prompt.ask("把当前 live 保存为什么 slot 名称").strip() if not name: note("已取消。") return save_live_to_slot(name) def tui_switch_slot() -> None: state = load_state() name = select_slot(state, "输入 slot 名称或序号") restore_slot_to_live(name, backup_current=True) def tui_login_and_save() -> None: name = Prompt.ask("登录后保存成哪个 slot").strip() if not name: note("已取消。") return extra = Prompt.ask("额外 login 参数(可留空)", default="").strip() auto = create_auto_backup() if auto: note(f"登录前自动备份: {auto['dir']}") code = run_claude(["login", *shlex.split(extra)]) note(f"claude login 退出码: {code}") if code == 0: save_live_to_slot(name) def add_account_flow(current_slot: str | None, new_slot: str | None, extra_args: list[str]) -> int: current_slot_name = require_name(current_slot, "当前账号 slot 名称") new_slot_name = require_name(new_slot, "新账号 slot 名称") ok("步骤 1/2:先保存当前 live 账号") save_live_to_slot(current_slot_name) ok("步骤 2/2:开始登录新账号,登录成功后自动保存") return login_and_save(new_slot_name, extra_args) def tui_add_account_flow() -> None: current_slot = Prompt.ask("当前 live 账号保存成哪个 slot").strip() if not current_slot: note("已取消。") return new_slot = Prompt.ask("新登录账号保存成哪个 slot").strip() if not new_slot: note("已取消。") return extra = Prompt.ask("额外 login 参数(可留空)", default="").strip() code = add_account_flow(current_slot, new_slot, shlex.split(extra)) note(f"新增账号流程退出码: {code}") def tui_logout_live() -> None: if not Confirm.ask("确认对当前 live 文件执行 claude logout ?", default=False): note("已取消。") return auto = create_auto_backup() if auto: note(f"logout 前自动备份: {auto['dir']}") code = run_claude(["logout"]) note(f"claude logout 退出码: {code}") def tui_launch_current() -> None: extra = Prompt.ask("额外 Claude 参数(可留空)", default="").strip() code = run_claude(shlex.split(extra)) note(f"Claude 退出码: {code}") def tui_switch_and_launch() -> None: state = load_state() name = select_slot(state, "输入要切换并启动的 slot") restore_slot_to_live(name, backup_current=True) extra = Prompt.ask("额外 Claude 参数(可留空)", default="").strip() code = run_claude(shlex.split(extra)) note(f"Claude 退出码: {code}") def show_tui() -> int: while True: state = load_state() console.clear() console.print(render_header(state)) console.print(render_live_panel(state)) console.print(render_slots_table(state)) console.print(render_auto_backups_table()) menu = Text.from_markup( "\n[bold]操作[/]\n" "[cyan]s[/] 保存当前 live 为 slot " "[cyan]x[/] 切换 slot 到 live\n" "[cyan]a[/] 一键新增账号(先保存当前,再登录新号)\n" "[cyan]l[/] 运行 claude login 并保存 " "[cyan]n[/] normalize live config " "[cyan]o[/] 备份后执行 claude logout\n" "[cyan]r[/] 直接启动当前 live Claude " "[cyan]y[/] 切换 slot 后启动 Claude\n" "[cyan]w[/] 查看当前 live 详情 " "[cyan]i[/] 查看某个 slot 详情\n" "[cyan]g[/] 运行 doctor 检查 " "[cyan]p[/] 查看路径 " "[cyan]d[/] 删除 slot\n" "[cyan]f[/] 刷新 " "[cyan]q[/] 退出" ) console.print(Panel(menu, title="Rich TUI", border_style="green")) action = Prompt.ask("选择操作", default="s").strip().lower() try: if action == "q": return 0 if action == "f": continue if action == "s": tui_save_slot() pause() continue if action == "x": tui_switch_slot() pause() continue if action == "a": tui_add_account_flow() pause() continue if action == "l": tui_login_and_save() pause() continue if action == "n": normalize_live_config(backup_current=True) pause() continue if action == "o": tui_logout_live() pause() continue if action == "r": tui_launch_current() pause() continue if action == "y": tui_switch_and_launch() pause() continue if action == "w": show_whoami(None) pause() continue if action == "i": state = load_state() name = select_slot(state, "输入要查看的 slot") show_whoami(name) pause() continue if action == "g": run_doctor() pause() continue if action == "p": target = Prompt.ask("输入 slot 名称(留空只看 live 路径)", default="").strip() show_paths(target or None) pause() continue if action == "d": state = load_state() name = select_slot(state, "输入要删除的 slot") if Confirm.ask(f"确认删除 slot {name} ?", default=False): remove_slot(name) pause() continue note(f"未知操作: {action}") pause() except SystemExit: raise except Exception as exc: console.print(f"[bold red]发生错误:[/] {exc}") pause() def print_help() -> None: help_text = """ [bold cyan]Claude 账号切换器(复制文件备份 + 直接修改 live 文件)[/] [bold]核心思路[/] - 直接操作官方 live 文件 - 切换前自动复制 live 文件做备份 - 再把已保存 slot 的文件覆盖回 live 路径 [bold]live 路径[/] - ~/.claude.json - ~/.claude/.config.json - ~/.claude/.credentials.json [bold]注意[/] - 为兼容 Claude 源码里 legacy 优先逻辑,恢复时会同步写入: ~/.claude.json 和 ~/.claude/.config.json - 这个版本 [yellow]不依赖[/] CLAUDE_CONFIG_DIR - 每个 slot 会保存并恢复自己的 [bold]userID[/], 也就是 .claude.json 里的 "userID" - [bold]推荐启用 normalize-live[/]:统一 .claude.json 和 .claude/.config.json, 避免你手工改其中一个后状态漂移 - macOS 上会额外备份 / 恢复 Keychain 里的 Claude OAuth 凭证 [bold]用法[/] python claude_switcher.py # Rich TUI python claude_switcher.py tui python claude_switcher.py add-account <当前slot> <新slot> [claude login 参数...] python claude_switcher.py doctor python claude_switcher.py normalize-live python claude_switcher.py list python claude_switcher.py save <slot> python claude_switcher.py switch [slot或序号] python claude_switcher.py use [slot或序号] python claude_switcher.py login <slot> [claude login 参数...] python claude_switcher.py logout python claude_switcher.py launch [slot或序号] [claude 参数...] python claude_switcher.py current python claude_switcher.py whoami [slot或序号|live] python claude_switcher.py paths [slot或序号|live] python claude_switcher.py remove [slot或序号] [bold]推荐流程[/] 1. 先用官方 claude 登录一个账号 2. 保存: python claude_switcher.py save work 3. 再登录另一个账号 4. 保存: python claude_switcher.py save personal 5. 之后切换: python claude_switcher.py switch work python claude_switcher.py switch personal [bold]一键新增账号[/] python claude_switcher.py add-account work personal 含义: - 先把当前 live 账号保存到 work - 再执行 claude login - 登录成功后自动把新账号保存到 personal [bold]Doctor 检查[/] python claude_switcher.py doctor 会检查: - 当前 live 文件是否存在 - 本机 Claude Code 版本是否能识别 - 每个 slot 的 userID / email / accountUuid 是否一致 - 是否存在多个账号共用同一个 userID 的冲突 [bold]Normalize Live[/] python claude_switcher.py normalize-live 含义: - 先自动备份当前 live - 再把当前 active config 同步写入: ~/.claude.json ~/.claude/.config.json [bold]序号选择[/] 执行 switch / whoami / paths / remove / launch 时, 不传 slot 名也可以,脚本会先把账号列表列出来, 然后让你输入序号选择。 """ console.print(Panel(Text.from_markup(help_text.strip()), border_style="cyan")) def launch_command(args: list[str]) -> int: if args: state = load_state() first = args[0] names = sorted(state["slots"]) if first in state["slots"] or first.isdigit(): selected = first if first.isdigit(): idx = int(first) if not (1 <= idx <= len(names)): fail(f"序号超出范围: {first}") selected = names[idx - 1] restore_slot_to_live(selected, backup_current=True) return run_claude(args[1:]) return run_claude(args) def login_and_save(name: str | None, extra_args: list[str]) -> int: slot_name = require_name(name, "slot 名称") auto = create_auto_backup() if auto: note(f"登录前自动备份: {auto['dir']}") code = run_claude(["login", *extra_args]) note(f"claude login 退出码: {code}") if code == 0: save_live_to_slot(slot_name) return code def logout_live() -> int: auto = create_auto_backup() if auto: note(f"logout 前自动备份: {auto['dir']}") return run_claude(["logout"]) def main(argv: list[str]) -> int: if not argv: return show_tui() command, *rest = argv if command not in RESERVED_COMMANDS: return launch_command([command, *rest]) if command in {"help", "--help", "-h"}: print_help() return 0 if command == "tui": return show_tui() if command in {"add-account", "add"}: return add_account_flow( rest[0] if len(rest) > 0 else None, rest[1] if len(rest) > 1 else None, rest[2:] if len(rest) > 2 else [], ) if command in {"doctor", "check"}: return 0 if run_doctor() else 1 if command in {"normalize-live", "normalize"}: normalize_live_config(backup_current=True) return 0 if command in {"list", "ls"}: list_slots() return 0 if command in {"save", "capture"}: save_live_to_slot(rest[0] if rest else None) return 0 if command in {"switch", "use"}: chosen = resolve_slot_input(rest[0] if rest else None, prompt_text="输入要切换的账号序号或名称") restore_slot_to_live(chosen, backup_current=True) return 0 if command == "login": return login_and_save(rest[0] if rest else None, rest[1:]) if command == "logout": return logout_live() if command in {"launch", "run"}: if not rest: chosen = resolve_slot_input(None, prompt_text="输入要启动的账号序号或名称") return launch_command([chosen]) return launch_command(rest) if command == "current": show_current() return 0 if command == "whoami": if rest and rest[0].strip().lower() == "live": show_whoami(None) else: chosen = resolve_slot_input(rest[0] if rest else None, prompt_text="输入要查看的账号序号或名称") show_whoami(chosen) return 0 if command == "paths": if rest and rest[0].strip().lower() == "live": show_paths(None) else: chosen = resolve_slot_input(rest[0] if rest else None, prompt_text="输入要查看路径的账号序号或名称") show_paths(chosen) return 0 if command == "env": print_env() return 0 if command in {"remove", "rm"}: chosen = resolve_slot_input(rest[0] if rest else None, prompt_text="输入要删除的账号序号或名称") remove_slot(chosen) return 0 print_help() return 0 if __name__ == "__main__": raise SystemExit(main(sys.argv[1:])) 4 个帖子 - 3 位参与者 阅读完整话题