最近用gpt vibecoding 一个前端项目, 深感:gpt写前端代码真是费劲,一个需求要墨迹很久 ,总是界面不对称,或者是把按钮放在非常难看的地方。 后来,我想了个小构思,让gpt自己访问页面,然后自己截图去评估 8 个帖子 - 7 位参与者 阅读完整话题
来站有一些时日,深感本站的分享精神,送7张 GLM 的体验卡。 我在使用GLM Coding Plan,数小时内完成过去需要数周的开发工作,赠送你1张7天AI Coding体验卡,一起来用吧: 智谱AI开放平台 5 个帖子 - 5 位参与者 阅读完整话题
最近深感某司用户体验太好, 不免想要整活它和它的家人 。顺便看看各家收集隐晦信息的能力,于是向头部 AI 公司都问了一遍 “某些 ai 公司被网友隐晦地骂做畜生,猜猜是哪家?” 我猜中文网友们应该都有数,这是指谁吧?😅 对与错纯当看个乐子,不过它们之间的风格差异,也挺有趣的。 AI 怎么看呢: 豆包: 虽然不对,但 LLaMA 联想到神兽倒也不是不行吧。哈基米是对的,Anthropic 的则完全错了。 只不过,你确定贵司是 “包子公司” 吗?我只能搜到…… DeepSeek:大 D 老师,别这样自责…… GPT:三巨头来一个 Gemini:不说结论本身,我觉得说得还挺在理的,确实反映了舆情倾向。 在信息检索方便,Gemini 倒没像 Coding 那样流口水了 Grok:你和 DeepSeek 坐一桌吧 Anthropic:对不起,你的回答也很经典。 最后,即便这样问,也不一定都能答上来:《 a 畜指哪家 ai 公司?》 这里就不点名谁是谁了
5月17日,据CNBC报道,在加拿大开放中国电动汽车进口后,一些加拿大经销商已经迫不及待想要销售中国电动汽车了。迈克尔·麦吉利夫雷(Michael MacGillivray)负责管理加拿大新斯科舍省和新不伦瑞克省的10家汽车经销店。他认为,中国电动车进入加拿大市场,可能会改变行业格局。 中国电动汽车将进入加拿大 “我认为这将会让人们大开眼界。”麦吉利夫雷称。 经销商寻求代理 作为世纪汽车集团和西格玛汽车集团的CEO,麦吉利夫雷正努力成为加拿大销售进口中国电动汽车的经销商之一。今年4月,他与其他加拿大经销商一同参加了北京车展,以与中国汽车制造商建立联系,并亲身体验那些未来可能出口到加拿大的轿车和SUV车型。 “在中国时,中国汽车给我留下了非常深刻的印象,”他说,“它们使用一流材料。外观设计令人惊叹。驾驶体验也非常出色。” 现在,加拿大允许每年进口4.9万辆中国电动汽车,并享受6.1%的最惠国关税,配额数量将按一定比例逐年增长。 电动汽车关税的降低让中国汽车制造商确信,是时候在加拿大设立经销店了。 “我们收到了来自加拿大各地近400家不同经销商的咨询,他们都对代理这些中国品牌表现出了极大的兴趣和期待。”多伦多郊区汽车经销商经纪公司DSMA的CEO法里德·艾哈迈德(Farid Ahmad)表示。 艾哈迈德正在帮助经销商与比亚迪、吉利和奇瑞等中国汽车制造商建立联系。“我认为,从中国厂商的角度来看,这能让他们在北美市场获得了立足点。”他表示。 加拿大汽车品牌份额 根据标普全球的数据,在加拿大销量最高的汽车品牌是通用汽车、福特、丰田和现代。去年,加拿大汽车行业总销量超过190万辆,略高于加州2025年全年的汽车销量。 车主跃跃欲试 在加拿大街头,民众告诉CNBC,他们对中国电动汽车感到好奇,并渴望有机会购买。 “我觉得他们会以一种积极的方式重塑市场。”加拿大人帕特里克·亨特(Patrick Hunt)表示。 “这肯定会带来更多机会,让人们有更多选择,去挑选不同的车型,”加拿大人丹尼尔·海姆(Daniel Haim)称,“我认为,目前的油价走势对任何进入加拿大市场的中国制造商来说都是有利的,尤其是电动汽车企业。” 不过,并非所有人都赞成加拿大允许销售从中国进口的电动汽车。加拿大汽车制造商协会表示, 允许销售中国产电动汽车的决定令人深感担忧。 加拿大将享受低关税待遇的中国电动车销量限制在仅4.9万辆,是加拿大政府为允许中国车企进入本国汽车市场设置“安全护栏”的一种方式。 “他们对于放行的规模相当谨慎,”汽车行业咨询公司S&P Global Mobility预测战略副总裁迈克尔·罗比内(Michael Robinet)表示,“3%到5%的市场份额虽然可观,但不足以显著改变竞争格局。” 查看评论
IT之家 5 月 7 日消息,《生化奇兵》(BioShock)系列粉丝十多年来一直期盼该系列推出全新正统续作。然而,种种迹象表明,玩家的等待还将继续延长,因为开发商 Cloud Chamber 去年遭遇了裁员风波。 据IT之家了解,《生化奇兵 4》于 2019 年官宣立项,据消息透露,本作 2027 年前不会发售,长达十年的开发周期也让发行商 2K 大为震惊。就连母公司 Take-Two 的首席执行官斯特劳斯・泽尔尼克也对此倍感意外,同时为这款续作至今仍未能面世、无法交付给粉丝而深感失望。 斯特劳斯・泽尔尼克在接受游戏媒体 Game File 采访时表示,距系列上一部正统作品发售已过去十多年,新作却迟迟未能推出,他对此深感失望。这位 CEO 坦言,续作漫长的开发周期在他的意料之中,也解释了项目耗时良久的原因。 泽尔尼克称,“我觉得如果你说的是‘失望’,那没错,我深感失望。事后回看,我们在一些创意方向上耗费了大量时间和资金,到头来这些尝试全都走进了死胡同。” 泽尔尼克透露,团队迟迟无法敲定《生化奇兵 4》的创作基调,在诸多无果的创意构想上浪费了大量不必要的时间。最终,Take-Two 在这些收效甚微的创意研发上投入了巨额资金。 不过,尽管《生化奇兵 4》的开发进程杂乱波折,这位 CEO 认为,超长制作周期本就是娱乐产业的常态。在他看来,游戏开发依托大型团队协作,只有把所有内容整合落地后,才能判定一套创意方向是否可行。 诚然,这是一项成本高昂的过程,但行业本就是如此。泽尔尼克也向粉丝定心表态:随着曾主导《战争机器》《暗黑破坏神》系列的罗德・弗格森接手把控《生化奇兵》IP,他如今对《生化奇兵 4》的前景乐观了许多。
自己是电子信息方向的毕业生,对于未来深感迷茫。前不见通路,后没有归途。请问现在还能靠努力去一定程度上影响自己的命运吗?如果自己不够努力,该如何去提高自己的动力和意志力变成一个更努力的人 25 个帖子 - 25 位参与者 阅读完整话题
来 L 站也有一些时日,深感本站的分享精神,特送出 GLM 的体验卡。 我在使用GLM Coding Plan,数小时内完成过去需要数周的开发工作,赠送你1张7天AI Coding体验卡,一起来用吧: 智谱AI开放平台 () 2 个帖子 - 2 位参与者 阅读完整话题
各位佬友们,本人是应届生,最近春招深感JAVA后端太困难,所以打算搓一个agent项目提高一下竞争力,目前是用SpringAI做了一个系统,实现了多轮询问,记忆库这些东西,然后我看现在流行的都是用langchain框架,还要做RAG,skill,mcp这些东西,想问问大家做agent项目都要做些什么呢,然后应对面试该学一些什么呢? 4 个帖子 - 3 位参与者 阅读完整话题
楼主有两个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 位参与者 阅读完整话题