我个人观察到的现象是,会话创建后的首条消息正常,之后的消息会延迟数秒才能发出,在 mac 和 windows 平台都出现了此问题; 并且 mac 版本还出现了运行时 cpu 使用率过高,发热严重的问题。 github 上也有不少类似的反馈,首个发现的版本应该是 26.415.21839 ;有人反映关闭记忆功能后可绕过此问题,但对大多数人包括我自己无效,目前只能通过降级解决。 github.com/openai/codex Fix: Desktop App - Message send is delayed for ~8 seconds in new sessions after the latest update 已打开 04:31AM - 17 Apr 26 UTC JavierPiedra bug app session ### What version of the Codex App are you using (From “About Codex” dialog)? 26 … .415.21839 ### What subscription do you have? Pro ### What platform is your computer? Darwin 25.3.0 arm64 arm ### What issue are you seeing? ## Summary In the Codex desktop app, sending a message can take around 8 seconds before the message is actually sent. This appears to have started after the latest update. It did not happen in older sessions before. ## Environment - App: Codex desktop app - Codex version: `26.415.21839 (1763)` - macOS: `26.3.1 (build 25D771280a)` - Hardware: `MacBook Air (Apple M3, 24 GB RAM)` ## Actual behavior After pressing Enter to send a message, the message does not send immediately. Observed behavior: - The spinner appears in the send button - The message remains pending for about `8 seconds` - Lowering reasoning does not help This is affecting sessions created after the latest update. ### What steps can reproduce the bug? ## Reproduction Observed repro: 1. Open Codex desktop 2. Open a newer thread created after the latest update 3. Type a normal message 4. Press Enter 5. Observe the spinner in the send button and a delay of about `8 seconds` before the message is sent Additional observation: - In a brand-new thread, the first message sent immediately - The second message in that same new thread then started taking a very long time to send ## Scope / pattern - Happens in newer sessions created after the latest update - Did **not** happen in older sessions tested by the user - Not fixed by lowering reasoning - Seen with `GPT-5.4` and `Extra High`, but reasoning level does not appear to be the cause ### What is the expected behavior? ## Expected behavior Pressing Enter should enqueue and send the message immediately, or at least show near-instant client acknowledgment. ### Additional information ## Notes This feels like client-side send latency rather than raw network latency. Local diagnostics from the machine at the time: - Network path looked healthy in macOS logs - Codex renderer/helper processes showed noticeable CPU activity while the issue was occurring - No obvious network failure was visible from the local system logs ## Impact This makes normal back-and-forth use of Codex frustrating because even short messages feel blocked before they are sent. If you want, I can also compress this into a shorter GitHub-style version, or turn it into a more technical issue with a small `Additional diagnostics` section at the end. 本以为是梯子又出问题,但看了下发现是 app 的锅,记录一下 9 个帖子 - 8 位参与者 阅读完整话题
也不能说是bug吧,只能说开发出来的东西能用。 稍微中型点的就像东拼西凑的一样,很难有很少的体验 需要多次打磨,也不知道是不是我要求太高了 以前ai都无法正常一个正常的应用,现在可以但是还觉得不够 各种框架 技能都试过了 但是总感觉不够好 我是只用codexapp了 9 个帖子 - 8 位参与者 阅读完整话题
IT之家 4 月 18 日消息,科技媒体 Linuxiac 昨日(4 月 17 日)发布博文,报道称开源兼容层 Wine 11.7 版本更新发布,在 Linux 与 macOS 平台上,持续优化运行 Windows 应用的兼容能力。 底层方面,本次更新最核心变动, 不再依赖 libxml2 库重构 MSXML ,从而提升底层处理 XML 的自主性与稳定性。 在脚本支持方面,新版显著增强 VBScript 功能,开发团队针对解析、控制流、常量、字典处理及行继续符等多个模块修复了大量错误。 多媒体功能层面,IT之家援引博文介绍,D3DX 新增支持 SRGB 滤镜,DirectSound 则引入了 7.1 扬声器配置。 修复方面,新版共计修复 35 个 BUG。应用层面修复了 ABBYY FineReader 12 Professional、VOCALOID6、Fade In Pro 及 Kakaowork 等软件的兼容性问题;游戏方面解决了 MapleStory World、Act of War Direct Action 等运行故障。 参考 Wine 11.7
免费用opus4.7太爽了,又搞定一个刷题app,基本达到 bug free,太爽了,还想再搞10个小时 14 个帖子 - 11 位参与者 阅读完整话题
不是真的假的为什么半个小时没掉额度啊不应该,没停过。难道是要到期了,忽悠我续费吗 9 个帖子 - 4 位参与者 阅读完整话题
OpenAI Developer Community – 17 Apr 26 [Security Report] Apple Pay receipt validation does not bind to purchaser... ChatGPT Bugs chatgpt api ⚠ Disclaimer: This report is for technical research and responsible disclosure purposes only. I do not endorse or encourage any unauthorized use, account sharing, or commercial exploitation of this finding. All testing was conducted on accounts I... Reading time: 1 min 🕑 Likes: 2 ❤ 1 个帖子 - 1 位参与者 阅读完整话题
Gemini app出现bug,无法对话,一直转圈圈,网页正常 3 个帖子 - 3 位参与者 阅读完整话题
内鬼急眼了 [Security Report] Apple Pay receipt validation does not bind to purchaser Apple ID – potential subscription bypass - ChatGPT / Bugs - OpenAI Developer Community 25 个帖子 - 21 位参与者 阅读完整话题
自己卡bug注册的 理论到月底4月30号 有50刀的额度 奖品详情: Grok API Key * 1 活动时间: 截止时间: 明天 12:00 参与方式: 在本帖下回复任意内容 抽奖规则: 每位用户仅允许参与一次。 使用官方抽奖工具随机抽取中奖者。 注意事项: 本活动将在活动截止时间后关闭回帖,以确保公正性。 中奖者将在活动结束后 12 小时内在本帖公布,并通过私信通知领奖方式。 所有规则及抽奖结果由活动发起人和论坛管理团队最终解释。 期待您的积极参与,祝您好运!如有任何疑问,欢迎随时联系抽奖发起人。 22 个帖子 - 20 位参与者 阅读完整话题
楼主有两个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 位参与者 阅读完整话题
本帖使用社区开源推广,符合推广要求。我申明并遵循社区要求的以下内容: 我的帖子已经打上 开源推广 标签: 是 我的开源项目完整开源,无未开源部分: 是 我的开源项目已链接认可 LINUX DO 社区: 是 我帖子内的项目介绍,AI生成、润色内容部分已截图发出: 是 以上选择我承诺是永久有效的,接受社区和佬友监督: 是 以下为项目介绍正文内容,AI生成、润色内容已使用截图方式发出 大家好,我最近在开发一个远程操控claudecode和codex的全栈项目:采取go后端直接跟claudecode交互,采取json数据流模式。 开发debug过程中遭遇了一个权限授予过程中的问题,让Gemeini搜索了才发现其来源于官方的bug也有相关的issue。 官方也给出了解决方案:解决方案为采用permission-model-auto 在此分享出来希望对有相同开发需求的人有帮助,大家对我这个项目有兴趣也欢迎浏览这个项目,最近正在持续修bug中完善后会单独推广的 github.com GitHub - JayCRL/MobileVC: Turn your phone into the control center for an AI... Turn your phone into the control center for an AI coding assistant CLI session (Claude or Codex) running on your computer. 1 个帖子 - 1 位参与者 阅读完整话题
GPT 的 bug 依旧不断啊,不知道啥时候修复这个东西,直接注册机一开,爽用啊 之前发了一点教程,可惜被举报了,那就只能看看了,方法依旧稳定 非烂大街的什么谷歌跟 iOS 订阅 7 个帖子 - 4 位参与者 阅读完整话题
出现问题的页面: @woshimahuateng 太阳之子 2025 年1 月 11 日加入 链接详情 <a href="http://vivgrid.com" rel="noopener nofollow ugc" data-clicks="201" aria-label="vivgrid.com 链接已点击 201 次" class="normal-external-link-icon">vivgrid.com</a> 规则参考文档: Discourse 论坛权限等级表 文档共建 Discourse 论坛权限等级表(包括版主角色) 这旨在成为一个全面的Discourse功能列表,按信任等级标记,并包括影响这些功能的相关管理员设置。如果你发现有遗漏或不正确的地方,请在下面留言。 信任等级表 – 默认值 TL = 依赖信任等级 = 可以由管理员设置启用或调整 TL0 TL1 TL2 TL3 TL4 … Discourse 论坛权限等级表 取消链接自动 nofollow 我这里也放个假链接吧 https://test.example.com 上面的链接也是如此: <a href="https://test.example.com" class="onebox normal-external-link-icon" target="_blank" rel="noopener nofollow ugc">https://test.example.com</a> 1 个帖子 - 1 位参与者 阅读完整话题
(话题已被作者删除) 1 个帖子 - 1 位参与者 阅读完整话题
Stream disconnected before completion: websocket closed by server before response.completed 如果佬们用过sub2api反代openai时如果打开websocket连接的话,一定会看到上面的提示,即使你在 账号管理 里对每个账号的 WS Mode 都开启了 passthrough 透传,你还是无法使用WS协议连接。 这就导致必须到 config.toml 配置文件里关掉站点的websocket支持,不然每次都会优先尝试WS协议去重试5次才会退回普通的流式。 今天心血来潮去sub2api的github上看了下,发现了官方合并了 Merge pull request from KnowSky404/fix/ws-codex-scheduler-cache-1662 这个pr。 由于官方还没有发版所以我本地pull打包了试了一下,发现还真是可以使用websocket协议了,不会再出现上面提示的错误了。 看了一下pr内容, 原来BUG产生的 原因是: scheduler_cache 在生成账号cache时没有把WS字段写入 redis cache。scheduler选账号时优先读取的是这个快照,而不是数据库原始账号,即使你把账号的 WS Mode 设为 透传(passthrough) ,scheduler 从 cache 看到的仍然是“没有 WS 能力”(或者说WS Mode被设置成关闭)的账号。即便 cache miss 后回源数据库重新构建 cache ,由于 cache 写入逻辑本身就漏字段,重建出来的仍然是错误 cache,所以 WS 选账号时会长期返回 no available OpenAI accounts。 现在opus 4.7也更新了,希望官方尽快发版吧,WS协议我的体感比普通的流式好很多。 2 个帖子 - 2 位参与者 阅读完整话题
是前端显示的bug还是重置了,还是gpt的bug? 3 个帖子 - 3 位参与者 阅读完整话题
Anthropic 开发者账号 @ClaudeDevs 上宣布修复一个 BUG:Claude 订阅的 5 小时与每周用量限额在处理 Opus 4.7 长上下文请求时统计有偏差,作为补偿已重置这两项限额。 修复呼应了近期用户的持续抱怨。Opus 4.6 支持 1M 上下文以来,GitHub 上积累了多条 Max/Pro 订阅用户的 BUG 报告,反映配额余量明显充足时,切换到 1M 上下文模式仍会立即弹出「Rate limit reached」。Opus 4.7 昨日发布又引入了新分词器,Anthropic 在迁移说明中承认相同输入可能消耗 1.0 至 1.35 倍 token,叠加计费偏差,实际扣掉的配额比用户感知的要多。 但补偿动作在一部分重度用户那里引发了反向不满。Claude 的每周限额以 7 天为一个周期,归零日期取决于账户各自的起始日而非统一周一。X 用户 Scott( @Dorizzdt )在推文中展示了这种错位带来的荒诞:他本周已刻意把使用率压在 43-47%,距离自然归零还剩约 24 小时,原本计划今天集中冲刺消耗剩余的 57%,再衔接下周满血启动。强制重置把已消耗的 43% 一笔勾销的同时,新的 7 天计时也从此刻立即重启,他今天再使用的任何额度都会一直挂在未来 7 天的窗口里。 换算下来,他本可以在 8 天内跑完「本周剩余 57% + 下周满血 100%」合计约 157% 的用量,现在被压缩成未来 7 天最多 100%。对一直满额消耗的用户,这次重置是纯福利;对像他这样刻意节制、等着攒到最后一天或换周再冲刺的用户,Anthropic 所谓的「补偿」反而抢走了他们攒出来的周差价。 这条插曲暴露了 Claude 订阅限额机制一个反直觉的特征:7 天周期对节制用户不友好,官方统一重置等同于「清零历史使用 + 重启计时器」,对挤爆配额的用户是恩惠,对克制用户则是强行预支未来额度。最稳妥的策略反而是:把当期配额尽快用完,不要攒。 1 个帖子 - 1 位参与者 阅读完整话题
我之前一个低价plus的GPT号今天刚掉了,变成free账号了,但是好像变成bug号了,额度一直100%,蹬了一个多小时了还没用完,有佬友遇到一样的情况吗? 1 个帖子 - 1 位参与者 阅读完整话题
这两个是不是不一样啊?好像出bug让我免费升级到heavy了? 4 个帖子 - 2 位参与者 阅读完整话题
【bug 描述】 L站顶部搜索框,输入文字。 此时按下方向键 预期:选择输入的文字 当前实际表现:触发了搜索下拉列表的选择,导致输入文字的选择被打断,变成了输入拼音 【复现环境】 chrome 浏览器 144+ mac os 15 1 个帖子 - 1 位参与者 阅读完整话题