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 from __future__ import annotations
@@ -30,7 +30,6 @@ def _secure_prompt_key(provider: Provider) -> str:
try: try:
return click.prompt(prompt_text, hide_input=True, err=True) return click.prompt(prompt_text, hide_input=True, err=True)
except click.UsageError: except click.UsageError:
# click.prompt raises when stdin is not a TTY. Fall back to /dev/tty.
try: try:
with open("/dev/tty", encoding="utf-8") as tty: # noqa: PTH123 with open("/dev/tty", encoding="utf-8") as tty: # noqa: PTH123
return tty.readline().rstrip("\n") return tty.readline().rstrip("\n")
@@ -40,16 +39,10 @@ def _secure_prompt_key(provider: Provider) -> str:
) from exc ) from exc
@click.group("backend") def provider_list_table() -> None:
def backend_cmd() -> None: """Print the provider list table (reusable from info command)."""
"""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."""
active = get_active_provider() active = get_active_provider()
console.print("[bold]Built-in backends:[/bold]") console.print("[bold]Available providers:[/bold]")
for provider in list_providers(): for provider in list_providers():
marker = "" marker = ""
if active and active.id == provider.id: if active and active.id == provider.id:
@@ -65,32 +58,43 @@ def backend_list() -> None:
) )
@backend_cmd.command("current") @click.group("provider")
def backend_current() -> None: def provider_cmd() -> None:
"""Show the active backend for this workspace.""" """Switch Claude Code's LLM provider (DeepSeek, Kimi, Kimi Code, Claude)."""
console.print(f"[bold]Current backend:[/bold] {describe_active_backend()}")
@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.argument("provider_id")
@click.option("--key", help="API key (scripting only; appears in shell history)") @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("--base-url", help="Override the provider base URL")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt") @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
def backend_use( def provider_use(
provider_id: str, provider_id: str,
key: str | None, key: str | None,
model: str | None, model: str | None,
base_url: str | None, base_url: str | None,
yes: bool, yes: bool,
) -> None: ) -> None:
"""Switch Claude Code to the given backend.""" """Switch to the given provider."""
try: try:
provider = get_provider(provider_id) provider = get_provider(provider_id)
except KeyError: except KeyError:
known = ", ".join(p.id for p in list_providers()) known = ", ".join(p.id for p in list_providers())
raise click.ClickException( raise click.ClickException(
f"Unknown backend '{provider_id}'. Choose from: {known}" f"Unknown provider '{provider_id}'. Choose from: {known}"
) )
if is_third_party(provider): if is_third_party(provider):
@@ -98,7 +102,7 @@ def backend_use(
if not resolved_key: if not resolved_key:
resolved_key = _secure_prompt_key(provider) resolved_key = _secure_prompt_key(provider)
if not resolved_key: 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: else:
resolved_key = None resolved_key = None
@@ -109,7 +113,7 @@ def backend_use(
summary += f" (base-url: {base_url})" summary += f" (base-url: {base_url})"
if not yes and not click.confirm( if not yes and not click.confirm(
f"Set backend to {summary} for this workspace?", f"Switch LLM provider to {summary}?",
default=True, default=True,
err=True, err=True,
): ):
@@ -125,37 +129,37 @@ def backend_use(
save_settings(settings) save_settings(settings)
set_setting("backend_provider", provider.id) 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": if provider.id == "claude":
console.print( 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") @click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
def backend_reset(yes: bool) -> None: def provider_reset(yes: bool) -> None:
"""Reset to the official Claude backend.""" """Reset to the official Claude provider."""
ctx = click.get_current_context() 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") @provider_cmd.group("key")
def backend_key() -> None: def provider_key() -> None:
"""Manage stored API keys.""" """Manage stored API keys."""
@backend_key.command("set") @provider_key.command("set")
@click.argument("provider_id") @click.argument("provider_id")
@click.option("--key", help="API key (scripting only; appears in shell history)") @click.option("--key", help="API key (scripting only; appears in shell history)")
def backend_key_set(provider_id: str, key: str | None) -> None: def provider_key_set(provider_id: str, key: str | None) -> None:
"""Store an API key for a backend without switching to it.""" """Store an API key without switching."""
try: try:
provider = get_provider(provider_id) provider = get_provider(provider_id)
except KeyError: except KeyError:
known = ", ".join(p.id for p in list_providers()) known = ", ".join(p.id for p in list_providers())
raise click.ClickException( raise click.ClickException(
f"Unknown backend '{provider_id}'. Choose from: {known}" f"Unknown provider '{provider_id}'. Choose from: {known}"
) )
if provider.id == "claude": 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.argument("provider_id")
@click.option( @click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt")
"--yes", def provider_key_rm(provider_id: str, yes: bool) -> None:
"-y", """Remove the stored API key for a provider."""
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."""
try: try:
provider = get_provider(provider_id) provider = get_provider(provider_id)
except KeyError: except KeyError:
known = ", ".join(p.id for p in list_providers()) known = ", ".join(p.id for p in list_providers())
raise click.ClickException( 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( if not yes and not click.confirm(
@@ -208,4 +207,4 @@ def backend_key_rm(provider_id: str, yes: bool) -> None:
if __name__ == "__main__": 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 Subcommands:
``current`` symlink, reinstalls the CLI tools from that runtime, and re-syncs version — switch the active runtime version (rollback/roll-forward)
the workspace. Run without an argument to list installed versions and pick agent — switch the default agent CLI (claude / kimi / codex / hermes)
one interactively. provider — switch Claude Code's LLM provider (DeepSeek / Kimi / Kimi Code / Claude)
""" """
from __future__ import annotations
import click import click
from rich.console import Console from rich.console import Console
from myagents.commands.sync_workspace import _print_report, sync_workspace from myagents.entrypoints import _KNOWN_BACKENDS
from myagents.commands.upgrade import _current_version, _install_tools, _runtime_root
from myagents.project_root import get_workspace_root from myagents.project_root import get_workspace_root
from myagents.settings import get_setting, set_setting
stderr_console = Console(stderr=True) stderr_console = Console(stderr=True)
console = Console() console = Console()
# ── version ────────────────────────────────────────────────────────────────
def _installed_versions() -> list[str]: def _installed_versions() -> list[str]:
from myagents.commands.upgrade import _runtime_root
root = _runtime_root() root = _runtime_root()
if not root.is_dir(): if not root.is_dir():
return [] return []
@@ -26,11 +32,14 @@ def _installed_versions() -> list[str]:
) )
@click.command("switch") @click.command("version")
@click.argument("version", required=False) @click.argument("version", required=False)
@click.option("--yes", is_flag=True, help="Skip the confirmation prompt") @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).""" """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() versions = _installed_versions()
current = _current_version() current = _current_version()
if not versions: 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] " f"\n[bold green]Switched: {current} -> {version}[/bold green] "
"(open a new terminal to pick up the change)" "(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(): def build_xiaohe_cli():
"""The ``xiaohe`` command group: agent forwarding + init/sync/upgrade/switch/uninstall/version.""" """The ``xiaohe`` command group: agent forwarding + info/init/sync/upgrade/switch/uninstall/version."""
from myagents.commands.backend import backend_cmd from myagents.commands.info import info_cmd
from myagents.commands.provider import provider_cmd
from myagents.commands.switch import switch_cmd from myagents.commands.switch import switch_cmd
from myagents.commands.sync_workspace import init_cmd, sync_cmd from myagents.commands.sync_workspace import init_cmd, sync_cmd
from myagents.commands.uninstall import uninstall_cmd from myagents.commands.uninstall import uninstall_cmd
from myagents.commands.upgrade import upgrade_cmd from myagents.commands.upgrade import upgrade_cmd
from myagents.commands.version import version_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 = 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(init_cmd)
xiaohe_cli.add_command(sync_cmd) xiaohe_cli.add_command(sync_cmd)
xiaohe_cli.add_command(upgrade_cmd) xiaohe_cli.add_command(upgrade_cmd)