智能助手网
标签聚合 可能

/tag/可能

linux.do · 2026-04-18 19:59:57+08:00 · tech

我买一个东西,要看看这个生产企业有没有被曝出剥削员工。一旦有,我一定不可能买。 原因有四: 一.剥削员工就是不把员工当人,一个不把员工当人的企业,凭什么让我相信它能把消费者当人?我不会买这种企业的东西。 二.员工被剥削、压榨,吃不好,受气,一定不可能好好做产品,会搞破坏。不是我把工人想象的坏,我要是那工人,我也会故意把螺丝拧松、把线接错、把焊点虚焊—— 凭什么我遭罪,你卖车赚钱? 没有这种道理。我不会买这种企业的东西。 三.一个剥削员工的企业,我买了它的产品,就是支持它,支持它剥削员工,是助纣为虐,我就成了帮凶。我不会买这种企业的东西。 四.我是工人的孩子,也是工人的预备役。我必须站在工人这一边,绝不能和奴隶主同流合污。我不会买这种企业的东西。 比如某车企在巴西被查出工地(外包)163名中国工人,30多人挤一间、共用1个厕所、无热水无通风,护照被没收、限制自由、克扣工资、长期加班······被巴西官方定性: 类似奴役的劳动 那么我永远不会买该企业的任何产品。 有些人笑了;“人家缺你一个吗?你不买有的是人买。”我要说,我不指望靠我一个人把它买死,我只要求我自己不做帮凶。 我不买,不是为了搞死谁,而是守住我的良心。我也是打工人,我以后也要进厂、也要上班、也要被人管。如果我今天买了剥削工人的企业的东西,就等于我在投票:“可以这么对待工人,我无所谓。”那以后别人剥削我的时候,我连骂人的资格都没有。 我不买,一个人确实没用,但千万个人像我一样,这种企业就死定了。这种人多一个,企业就怕一分。 我不买,就是在平静地告诉所有剥削员工的企业、自甘堕落的工贼: 我鄙视你,我不和你玩,我绝不顺从你。 我不买你的产品,那是你的损失,不是我的。我有自己的底线,这比所有的口号都重要。 这就是我买东西的一个准则。 10 个帖子 - 6 位参与者 阅读完整话题

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 16:59:15+08:00 · tech

个人声明 本人为小白,只是提出一种可能的猜测,分享出来,并非绝对如此,请理性看待。 本人并非专业人士,请保持怀疑态度。 疑似被阉割的Claude Opus 4.7 2026年4月16日,A社发布了它们的新模型。 在发布后的几天内,几乎如潮水般的吐槽涌现——人们纷纷表示新模型的问题令人烦恼,我简单总结几点: 1.不说人话 2.上下文倒退 3.分词器更换,价格变相增高。 4.自适应思考不稳定 此外,A社着重削弱了其在网络安全方面的能力,相较于Opus 4.6模型,低了0.7% 项目 Claude Opus 4.7 Claude Opus 4.6 Cybersecurity vulnerability reproduction (CyberGym) 73.1% 73.8% 从中,我似乎感觉到一种感受:这个最新的Opus 4.7 模型,甚至并不如前作Opus4.6,或者说,简直像换了一个模型。 甚至诞生了关于它们的有趣图片: Opus 4.7 Opus 4.6 的确,Opus 4.7在各个方面似乎都差于Opus 4.6,不稳定…记不住事情…说话像GPT,这甚至可能令人思考——这到底是不是一个新发布的Opus模型。 神秘的Claude Mythos Preview 在Opus 4.7 推出前,A社曾泄露了它们的前沿模型消息,Mythos(卡皮巴拉),借着这张模型对比图,我们也许可以略窥一二它可能达到的强大(因为截至目前为止,大部分人无法使用这个模型,A社向部分B端提供此模型,并且貌似放弃了C端市场) Claude Mythos Preview 几乎全面领先于 Opus 4.7,也远远超越其他模型。 当然,它的价格让人无奈,我列出一个表格来使对比更清晰对比 模型 输入价格 (每百万 token) 输出价格 (每百万 token) Claude Mythos Preview $25 $125 Claude Opus 4.6/4.7 $5 $25 Claude Sonnet 4.6 $3 $15 Claude Haiku 4.5 $1 $5 Claude Mythos Preview的价格相较于Opus翻了5倍,这种恐怖的价格也许与其的模型参数量有所关系,大多人猜测,这个价格恐怖的模型大概率参数量一样是 Opus 模型的数倍。 订阅套餐的额度提升 翻译:Opus 4.7 使用了更多思考代币,因此我们提高了所有订阅者的速率限制以弥补这一点。祝你玩得开心! 据佬友测试,pro账号的5h,7d,可能已经全部翻倍,其他订阅目前我尚未得知。 这与我的标题有什么关系? 我们可以将前面的线索串联起来,首先,A社发布了一个新的Opus(4.7)模型,而此模型,综合方面甚至不如旧版本的Opus(4.6)。 那么,推出这个模型的目的大概率不是为了让人去辱骂它们(因为它们大概率已经自己使用过了,也应该知道这个最新模型的问题。) 在前面我们得知,Claude Mythos Preview 的价格是极为昂贵的,且不面向大多数C端。 而这时有一个很重要的问题需要确认,因为它关乎我的论点。 那就是——Claude Mythos Preview 是怎么来的:我猜想它是源于Opus 4.6。 此外,我需要提到一种可能,Claude Mythos Preview也许有可能是A社重塑的一个全新模型,与Opus 4.6 毫无瓜葛,这都是可能是,所以理性看待我的想法——它并不准确。 Opus 4.6 几乎是一个强大的模型,它稳定,上下文强大,说话清晰易懂,几乎没有任何语料污染。 也许在此基础上,A社投入巨大将其的参数量堆到一个恐怖的规模,造就Claude Mythos Preview 的恐怖能力,但同时,它的价格也过于高昂,导致无法完美的使用它(算力,种种因素束缚着这个模型) 同时,A社也许自认为已经将这条路堆到了极限。 所以它们需要转型——Opus 4.7。 一个很有意思的点是,Opus 4.7 使用了新的分词器,我并没有学习过大模型的种种,但我意识到这可能是一个巨大的变化,A社重构了Opus 4.7这个模型,目前的新模型是一个新的基础开始。 请辩证看待,怀疑看待,这段话可能不对,我并不了解大模型,我只是猜测。 并且这也许有所佐证,Opus 4.7 与Opus 4.6的上下文简直是大变样。 所以我提出一个大胆的假设: Claude Mythos Preview 是一个已经走到头的方向,它昂贵,且难以进步 所以A社推出了新的模型——Opus 4.7 面向B端进行试水,它大概率是一个全新的模型,用于转型,他们也许希望制造出更便宜,更好用的模型,因为原有的路可能无法走通。 而这时,庞大的C端是一个完美的地方。 大概率佬友们都知道,A社的官方Claude订阅额度是非常之高的,在A社增加所有订阅配额前,大概是这样 套餐 5h $ 限额 7d $ 限额 月 $ 限额 Pro ($20) $4.1 $37.5 $163 Max 5× ($100) $24.8 $312.5 $1,354 Max 20× ($200) $82.5 $625.0 $2,708 而如今参考pro订阅翻2倍的案例,我们得知,pro额度目前的月限额大概为:300美刀。 我不能去揣测max套餐的额度,因为我并没有这方面的知识,所以我们假设——这三个套餐都有一定量的提升。 但是为什么? 为什么A社这个几乎抠门的骨子里的公司要提升额度??? 我们知道,目前的订阅已经是赔本了(即使会降智,我猜测这是一种采集完数据后降本的策略,因为前几个月已经采集到了足够的信息) 那么它为什么要再次提升额度? 我想,是因为新的Opus 4.7 模型,如果A社想要获得更多信息,那么提高配额是一个绝妙的点子。 能看出来其实目前的A社套餐便都是赔本订阅,人们花着极少的钱购买到了远超数倍的体验。 A社为什么要推出这些套餐?A社是为了打市场吗?我想——这可能与模型有关。 每次出新模型后都会高智商,随后降智——这也许是吸取到了足够数据后的降本。 而目前,Opus 4.6依旧降智,人们需要去使用Opus 4.7 ,全新的更多配额让更多的人去使用Opus 4.7,那样的数据也许是极为重要的。 5 个帖子 - 4 位参与者 阅读完整话题

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

项目 这是一个 Unity C# 项目,我进行测试的是一份皮肤系统需求案,我已经做了好预制体,而模型需要编写代码。 本轮与上两轮评测的项目和环境都完全一致: 第一轮 … 第九轮 模型来源 Claude Opus 4.7: 宣称官方 Claude Code 的中转站。 速度 排名 模型 时间(分钟) 备注 1 Grok 4.20 0309 Reasoning 3 2 Minimax M2.1 5 3 Minimax M2.5 6 4 Step-3.5-Flash 6 5 Mimo V2 Omni 7 6 Doubao-Seed-2.0-Lite 7 7 GPT-5.4(low) 8 8 Doubao-Seed-2.0-Pro 9 9 Doubao-Seed-2.0-Code 9 10 Qwen3-Coder-Next 9 11 Claude Sonnet 4.6(high) 9 12 Qwen3.5-Plus 9 13 GLM-5 Turbo 10 14 Minimax M2.7 10 Highspeed 版本 15 Qwen3.5-Flash 10 16 GPT-5.3-Codex(medium) 10 17 Gemini 3 Pro 11 18 Kimi K2.5 11 19 GLM 4.7 12 20 GPT-5.4(high) 14 21 Mimo V2 Pro 15 22 Claude Opus 4.5 15 23 Claude Sonnet 4.5 16 24 GPT-5.3-Codex(high) 16 触发了一次上下文压缩 25 GPT-5.3-Codex(xhigh) 16 26 GPT-5.4(medium) 17 27 GPT-5.4(xhigh) 18 28 Claude-Opus-4.7(Max) 20 29 GLM-5 20 30 DeppSeek V3.2 22 31 Gemini 3 Flash 22 32 KAT-Coder-Pro V2 24 33 GPT 5.2(xhigh) 25 34 Claude-Opus-4.6(Max) 26 35 Gemini 3.1 Pro(high) 29 受 429 请求频率限制影响 36 Qwen3.5 9B GGUF Q4_K_XL 35 MBP M4 Pro 48GB 本地部署 37 Qwen3.5 35B A3B GGUF Q4_K_XL 36 MBP M4 Pro 48GB 本地部署 令牌数 Claude Opus 4.7: 4.89M 代码行数 Claude Opus 4.7: +1473, -8 完成度 Claude Opus 4.7 审查结论: 已经存在必然编译失败,且核心入口链路未打通。 详细 (点击了解更多详细信息) 代码质量 经典 Claude 风格,无需多言。 最终总结 排名 模型/层级 说明 Tier 0 该等级的模型实现与线上基线高度一致。 1 GPT 5.4(xhigh) 2 GPT 5.2(xhigh) 3 GPT-5.3-Codex(xhigh) Tier 1 该等级的模型的代码正确完整且可编译,仅少量边界问题或轻微不一致。 4 GPT 5.4(high) 5 GPT 5.4(medium) 6 GPT-5.3-Codex(high) 7 GPT-5.3-Codex(medium) 8 Claude Opus 4.6(Max) 9 GPT 5.2(medium) 10 GPT 5.4(low) 11 GPT 5.2 Codex(xhigh) 12 Claude Opus 4.5 13 Claude Sonnet 4.5 Tier 2 该等级的模型的代码至少可编译或仅极少量的语法错误,但是存在明显功能错误、遗漏或与需求/线上不一致。 14 GLM 5.1 15 GLM 5 16 Kimi K2.5 17 Claude Sonnet 4.6(high) 18 Qwen3.5-Plus 19 KAT-Coder-Pro V2 Tier 3 该等级的模型的问题很多且无法编译,或者存在不少幻觉。 20 Claude Opus 4.7(Max) 21 GLM 5 Turbo 22 GLM 4.7 23 Gemini 3.1 Pro(high) 24 Mimo V2 Pro 25 Mimo V2 Omni 26 Minimax M2.7 27 Minimax M2.5 28 Step-3.5-Flash 29 Qwen3-Coder-Next 30 Gemini 3 Pro 31 Gemini 3 Flash 32 Doubao-Seed-2.0-Code 33 Doubao-Seed-2.0-Pro 34 Doubao-Seed-2.0-Lite 35 Qwen3.5-Flash 36 Qwen3.5 35B A3B GGUF Q4_K_XL 37 Qwen3.5 9B GGUF Q4_K_XL 38 Grok 4.20 0309 Reasoning 39 DeepSeek V3.2 40 Minimax M2.1 41 GPT 5.1 Codex mini(medium) 使用中文对 Opus 4.7 提问,在完成的过程中是全英文的,但是最后的总结输出是中文。 速度相对上一代快了 6 分钟(23%)。 出现两个致命的编译错误,看来注意力低不假。 之前模型犯的错误,这次也同样犯了。 太抽象了,我甚至给了 3 次机会,怕我误会了它,但是结果依然没有改变,现在 T3 排行由 Opus 4.7 重磅领衔,后续的 T3 级别选手恐怕短时间难以超越。 本次继续使用自己开发的开源 VS Code 插件 Unify Chat Provider 以实现在 Copilot 中使用以上模型。 17 个帖子 - 12 位参与者 阅读完整话题

www.ithome.com · 2026-04-18 12:51:01+08:00 · tech

IT之家 4 月 18 日消息,当地时间 4 月 17 日,据外媒 Wccftech 报道,英伟达 CEO 黄仁勋在斯坦福大学商学院发言时表示,人工智能不会取代人类,而将成为新的工作平台, 并在长期内创造更多就业机会 。 黄仁勋再次将 AI 比作现代工业革命,强调“AI 是一项每个人都应该掌握的技术”。 围绕 AI 对就业的影响,外界长期存在分歧。一方面,AI 显著提升效率;另一方面,不少人担心自身岗位被取代。对此,黄仁勋表示,“人们更可能被 会使用 AI 的人 取代,而不是被 AI 本身取代,因此关键在于让所有人掌握这项工具。” 黄仁勋表示,AI 正在改变职业路径。例如, 一些原本从事木工的人可以借助 AI 转型为建筑设计师 ,通过输入需求即可获得高质量设计方案,也可以进入室内设计等领域,从而提升技能和服务能力。AI 之所以成为史上普及速度最快的技术之一,正是因为其易用性,使更多人能够参与其中。 从长期来看,AI 将带来更多就业机会,关键在于 将其视为创造岗位的平台 。人类发展的核心在于适应能力,能够率先将 AI 融入工作的群体,将在这一轮变革中受益最大。“总体而言,我相信最终就业机会会增加,这场工业革命 结束时的就业人数将高于开始时 ,就像历史上的工业革命一样。” 为支撑这一趋势,英伟达及全球企业正持续开发新的 AI 应用场景。从早期较为基础的技术,到生成式 AI 带来的快速变化,再到当前受到关注的 AI 智能体,围绕企业需求的生态体系正在形成。 与此同时,AI 也引发争议。以 DLSS 5 为例,这项技术旨在通过 AI 提升游戏画面,发布后引发广泛讨论,一些厂商表态支持,但也有创作者担心其影响原有艺术风格。据IT之家了解,英伟达对此回应称,该技术会尊重创作者的原始意图。

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 位参与者 阅读完整话题