refactor(backend): backend 改名 provider,拆出 info 子命令

This commit is contained in:
Zhengshou Lai
2026-07-16 21:07:16 +08:00
parent e021aa9dbf
commit dd2011ae2d
4 changed files with 192 additions and 57 deletions
+51
View File
@@ -0,0 +1,51 @@
"""``xiaohe info`` — show current version, agent, and provider."""
from __future__ import annotations
from importlib.metadata import PackageNotFoundError, version
import click
from rich.console import Console
from rich.table import Table
console = Console()
def _package_version() -> str:
try:
return version("myagents")
except PackageNotFoundError:
return "0.0.0"
def _describe_runtime() -> str:
from myagents.commands.upgrade import _current_version
cur = _current_version()
return cur if cur else "development"
@click.command("info")
def info_cmd() -> None:
"""Show xiaohe status: version, agent, and provider."""
from myagents.commands.switch import _resolve_agent
from myagents.claude_settings import describe_active_backend
from myagents.commands.provider import provider_list_table
table = Table(show_header=False, box=None, padding=(0, 1))
table.add_column("key", style="dim")
table.add_column("value")
table.add_row("Version", f"myagents {_package_version()} (runtime: {_describe_runtime()})")
default_agent = _resolve_agent()
table.add_row("Agent", f"{default_agent} (xiaohe → my{default_agent})")
table.add_row("Provider", describe_active_backend())
console.print(table)
console.print()
provider_list_table()
console.print()
console.print("[dim]Switch with:[/dim]")
console.print(" xiaohe switch version xiaohe switch agent xiaohe switch provider")
@@ -1,4 +1,4 @@
"""``xiaohe backend`` — switch Claude Code's model backend."""
"""``xiaohe switch provider`` — switch Claude Code's LLM provider."""
from __future__ import annotations
@@ -30,7 +30,6 @@ def _secure_prompt_key(provider: Provider) -> str:
try:
return click.prompt(prompt_text, hide_input=True, err=True)
except click.UsageError:
# click.prompt raises when stdin is not a TTY. Fall back to /dev/tty.
try:
with open("/dev/tty", encoding="utf-8") as tty: # noqa: PTH123
return tty.readline().rstrip("\n")
@@ -40,16 +39,10 @@ def _secure_prompt_key(provider: Provider) -> str:
) from exc
@click.group("backend")
def backend_cmd() -> None:
"""Switch Claude Code's model backend (DeepSeek, Kimi, Kimi Code, Claude)."""
@backend_cmd.command("list")
def backend_list() -> None:
"""List built-in backends and the active one."""
def provider_list_table() -> None:
"""Print the provider list table (reusable from info command)."""
active = get_active_provider()
console.print("[bold]Built-in backends:[/bold]")
console.print("[bold]Available providers:[/bold]")
for provider in list_providers():
marker = ""
if active and active.id == provider.id:
@@ -65,32 +58,43 @@ def backend_list() -> None:
)
@backend_cmd.command("current")
def backend_current() -> None:
"""Show the active backend for this workspace."""
console.print(f"[bold]Current backend:[/bold] {describe_active_backend()}")
@click.group("provider")
def provider_cmd() -> None:
"""Switch Claude Code's LLM provider (DeepSeek, Kimi, Kimi Code, Claude)."""
@backend_cmd.command("use")
@provider_cmd.command("list")
def provider_list() -> None:
"""List available providers and the active one."""
provider_list_table()
@provider_cmd.command("current")
def provider_current() -> None:
"""Show the active provider."""
console.print(f"[bold]Active provider:[/bold] {describe_active_backend()}")
@provider_cmd.command("use")
@click.argument("provider_id")
@click.option("--key", help="API key (scripting only; appears in shell history)")
@click.option("--model", help="Override the default model for this provider")
@click.option("--model", help="Override the default model")
@click.option("--base-url", help="Override the provider base URL")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
def backend_use(
def provider_use(
provider_id: str,
key: str | None,
model: str | None,
base_url: str | None,
yes: bool,
) -> None:
"""Switch Claude Code to the given backend."""
"""Switch to the given provider."""
try:
provider = get_provider(provider_id)
except KeyError:
known = ", ".join(p.id for p in list_providers())
raise click.ClickException(
f"Unknown backend '{provider_id}'. Choose from: {known}"
f"Unknown provider '{provider_id}'. Choose from: {known}"
)
if is_third_party(provider):
@@ -98,7 +102,7 @@ def backend_use(
if not resolved_key:
resolved_key = _secure_prompt_key(provider)
if not resolved_key:
raise click.ClickException("API key is required for this backend.")
raise click.ClickException("API key is required for this provider.")
else:
resolved_key = None
@@ -109,7 +113,7 @@ def backend_use(
summary += f" (base-url: {base_url})"
if not yes and not click.confirm(
f"Set backend to {summary} for this workspace?",
f"Switch LLM provider to {summary}?",
default=True,
err=True,
):
@@ -125,37 +129,37 @@ def backend_use(
save_settings(settings)
set_setting("backend_provider", provider.id)
console.print(f"[bold green]Backend switched to {summary}.[/bold green]")
console.print(f"[bold green]Switched to {summary}.[/bold green]")
if provider.id == "claude":
console.print(
"[dim]Cleared third-party backend env vars from ~/.claude/settings.json.[/dim]"
"[dim]Cleared third-party provider env vars from ~/.claude/settings.json.[/dim]"
)
@backend_cmd.command("reset")
@provider_cmd.command("reset")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
def backend_reset(yes: bool) -> None:
"""Reset to the official Claude backend."""
def provider_reset(yes: bool) -> None:
"""Reset to the official Claude provider."""
ctx = click.get_current_context()
ctx.invoke(backend_use, provider_id="claude", yes=yes)
ctx.invoke(provider_use, provider_id="claude", yes=yes)
@backend_cmd.group("key")
def backend_key() -> None:
@provider_cmd.group("key")
def provider_key() -> None:
"""Manage stored API keys."""
@backend_key.command("set")
@provider_key.command("set")
@click.argument("provider_id")
@click.option("--key", help="API key (scripting only; appears in shell history)")
def backend_key_set(provider_id: str, key: str | None) -> None:
"""Store an API key for a backend without switching to it."""
def provider_key_set(provider_id: str, key: str | None) -> None:
"""Store an API key without switching."""
try:
provider = get_provider(provider_id)
except KeyError:
known = ", ".join(p.id for p in list_providers())
raise click.ClickException(
f"Unknown backend '{provider_id}'. Choose from: {known}"
f"Unknown provider '{provider_id}'. Choose from: {known}"
)
if provider.id == "claude":
@@ -175,22 +179,17 @@ def backend_key_set(provider_id: str, key: str | None) -> None:
)
@backend_key.command("rm")
@provider_key.command("rm")
@click.argument("provider_id")
@click.option(
"--yes",
"-y",
is_flag=True,
help="Skip the confirmation prompt",
)
def backend_key_rm(provider_id: str, yes: bool) -> None:
"""Remove the stored API key for a backend."""
@click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt")
def provider_key_rm(provider_id: str, yes: bool) -> None:
"""Remove the stored API key for a provider."""
try:
provider = get_provider(provider_id)
except KeyError:
known = ", ".join(p.id for p in list_providers())
raise click.ClickException(
f"Unknown backend '{provider_id}'. Choose from: {known}"
f"Unknown provider '{provider_id}'. Choose from: {known}"
)
if not yes and not click.confirm(
@@ -208,4 +207,4 @@ def backend_key_rm(provider_id: str, yes: bool) -> None:
if __name__ == "__main__":
backend_cmd()
provider_cmd()
+91 -9
View File
@@ -1,23 +1,29 @@
"""``xiaohe switch`` — move ``current`` to another installed runtime version.
"""``xiaohe switch`` — switch version, agent, or provider.
Rollback/roll-forward counterpart to ``xiaohe upgrade``: repoints the
``current`` symlink, reinstalls the CLI tools from that runtime, and re-syncs
the workspace. Run without an argument to list installed versions and pick
one interactively.
Subcommands:
version — switch the active runtime version (rollback/roll-forward)
agent — switch the default agent CLI (claude / kimi / codex / hermes)
provider — switch Claude Code's LLM provider (DeepSeek / Kimi / Kimi Code / Claude)
"""
from __future__ import annotations
import click
from rich.console import Console
from myagents.commands.sync_workspace import _print_report, sync_workspace
from myagents.commands.upgrade import _current_version, _install_tools, _runtime_root
from myagents.entrypoints import _KNOWN_BACKENDS
from myagents.project_root import get_workspace_root
from myagents.settings import get_setting, set_setting
stderr_console = Console(stderr=True)
console = Console()
# ── version ────────────────────────────────────────────────────────────────
def _installed_versions() -> list[str]:
from myagents.commands.upgrade import _runtime_root
root = _runtime_root()
if not root.is_dir():
return []
@@ -26,11 +32,14 @@ def _installed_versions() -> list[str]:
)
@click.command("switch")
@click.command("version")
@click.argument("version", required=False)
@click.option("--yes", is_flag=True, help="Skip the confirmation prompt")
def switch_cmd(version: str | None, yes: bool) -> None:
def switch_version(version: str | None, yes: bool) -> None:
"""Switch the active runtime to another installed version (rollback)."""
from myagents.commands.sync_workspace import _print_report, sync_workspace
from myagents.commands.upgrade import _current_version, _install_tools, _runtime_root
versions = _installed_versions()
current = _current_version()
if not versions:
@@ -82,3 +91,76 @@ def switch_cmd(version: str | None, yes: bool) -> None:
f"\n[bold green]Switched: {current} -> {version}[/bold green] "
"(open a new terminal to pick up the change)"
)
# ── agent ──────────────────────────────────────────────────────────────────
def agent_list_table() -> None:
"""Print the agent list (reusable from info command)."""
default = _resolve_agent()
console.print("[bold]Available agents:[/bold]")
for name in _KNOWN_BACKENDS:
marker = " [green](current)[/green]" if name == default else ""
console.print(f" {name}{marker}")
def _resolve_agent() -> str:
import re
agent = str(get_setting("default_agent", "") or "")
agent = re.sub(r"^my", "", agent.strip().lower())
return agent if agent in _KNOWN_BACKENDS else "claude"
@click.command("agent")
@click.argument("agent", required=False)
def switch_agent(agent: str | None) -> None:
"""Switch the default agent CLI."""
known = list(_KNOWN_BACKENDS)
default = _resolve_agent()
if not agent:
console.print("[bold]Available agents:[/bold]")
for name in known:
marker = " [green](current)[/green]" if name == default else ""
console.print(f" {name}{marker}")
agent = str(click.prompt("Switch to", err=True))
agent = agent.strip().lower()
if agent.startswith("my"):
agent = agent[2:]
if agent not in _KNOWN_BACKENDS:
raise click.ClickException(
f"Unknown agent '{agent}'. Choose from: {', '.join(known)}"
)
if agent == default:
console.print(f"Already on {agent}.")
return
set_setting("default_agent", agent)
console.print(f"[bold green]Switched default agent to {agent}.[/bold green]")
console.print(f"[dim]Use 'xiaohe' (or 'my{agent}') to launch.[/dim]")
# ── switch group ───────────────────────────────────────────────────────────
@click.group("switch", invoke_without_command=True)
@click.pass_context
def switch_cmd(ctx: click.Context) -> None:
"""Switch version, agent, or provider.
Run without arguments to see all options.
"""
if ctx.invoked_subcommand is None:
console.print("[bold]Use one of:[/bold]")
console.print(" xiaohe switch version — switch runtime version")
console.print(" xiaohe switch agent — switch default agent CLI")
console.print(" xiaohe switch provider — switch LLM provider")
console.print()
console.print("[dim]Run 'xiaohe switch <subcommand> --help' for details.[/dim]")
switch_cmd.add_command(switch_version)
switch_cmd.add_command(switch_agent)
# provider_cmd is added in entrypoints.py (lazy import to avoid cycles)
+6 -3
View File
@@ -26,16 +26,19 @@ def default_agent() -> str:
def build_xiaohe_cli():
"""The ``xiaohe`` command group: agent forwarding + init/sync/upgrade/switch/uninstall/version."""
from myagents.commands.backend import backend_cmd
"""The ``xiaohe`` command group: agent forwarding + info/init/sync/upgrade/switch/uninstall/version."""
from myagents.commands.info import info_cmd
from myagents.commands.provider import provider_cmd
from myagents.commands.switch import switch_cmd
from myagents.commands.sync_workspace import init_cmd, sync_cmd
from myagents.commands.uninstall import uninstall_cmd
from myagents.commands.upgrade import upgrade_cmd
from myagents.commands.version import version_cmd
switch_cmd.add_command(provider_cmd)
xiaohe_cli = build_cli(default_agent(), prog_name="xiaohe", offer_install=True)
xiaohe_cli.add_command(backend_cmd)
xiaohe_cli.add_command(info_cmd)
xiaohe_cli.add_command(init_cmd)
xiaohe_cli.add_command(sync_cmd)
xiaohe_cli.add_command(upgrade_cmd)