mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-07 02:05:51 +00:00
feat(cli): add provider logout command
- Implement \ anobot provider logout <provider>\ to clear OAuth credentials. - Add \_LOGOUT_HANDLERS\ registration mechanism mirroring login. - Implement logout for \openai-codex\ by deleting local \oauth-cli-kit\ token and lock files. - Fallback gracefully when attempting to logout from providers lacking local credentials or implementations. - Fixes #2665
This commit is contained in:
parent
0f32c0451e
commit
387988b8e9
@ -1487,10 +1487,12 @@ provider_app = typer.Typer(help="Manage providers")
|
||||
app.add_typer(provider_app, name="provider")
|
||||
|
||||
|
||||
_LOGIN_HANDLERS: dict[str, callable] = {}
|
||||
_LOGIN_HANDLERS: dict[str, Any] = {}
|
||||
_LOGOUT_HANDLERS: dict[str, Any] = {}
|
||||
|
||||
|
||||
def _register_login(name: str):
|
||||
"""注册 OAuth 登录处理器。"""
|
||||
def decorator(fn):
|
||||
_LOGIN_HANDLERS[name] = fn
|
||||
return fn
|
||||
@ -1498,11 +1500,16 @@ def _register_login(name: str):
|
||||
return decorator
|
||||
|
||||
|
||||
@provider_app.command("login")
|
||||
def provider_login(
|
||||
provider: str = typer.Argument(..., help="OAuth provider (e.g. 'openai-codex', 'github-copilot')"),
|
||||
):
|
||||
"""Authenticate with an OAuth provider."""
|
||||
def _register_logout(name: str):
|
||||
"""注册 OAuth 登出处理器。"""
|
||||
def decorator(fn):
|
||||
_LOGOUT_HANDLERS[name] = fn
|
||||
return fn
|
||||
return decorator
|
||||
|
||||
|
||||
def _resolve_oauth_provider(provider: str):
|
||||
"""解析并校验 OAuth provider 配置。"""
|
||||
from nanobot.providers.registry import PROVIDERS
|
||||
|
||||
key = provider.replace("-", "_")
|
||||
@ -1511,6 +1518,15 @@ def provider_login(
|
||||
names = ", ".join(s.name.replace("_", "-") for s in PROVIDERS if s.is_oauth)
|
||||
console.print(f"[red]Unknown OAuth provider: {provider}[/red] Supported: {names}")
|
||||
raise typer.Exit(1)
|
||||
return spec
|
||||
|
||||
|
||||
@provider_app.command("login")
|
||||
def provider_login(
|
||||
provider: str = typer.Argument(..., help="OAuth provider (e.g. 'openai-codex', 'github-copilot')"),
|
||||
):
|
||||
"""Authenticate with an OAuth provider."""
|
||||
spec = _resolve_oauth_provider(provider)
|
||||
|
||||
handler = _LOGIN_HANDLERS.get(spec.name)
|
||||
if not handler:
|
||||
@ -1521,6 +1537,22 @@ def provider_login(
|
||||
handler()
|
||||
|
||||
|
||||
@provider_app.command("logout")
|
||||
def provider_logout(
|
||||
provider: str = typer.Argument(..., help="OAuth provider (e.g. 'openai-codex', 'github-copilot')"),
|
||||
):
|
||||
"""Log out from an OAuth provider."""
|
||||
spec = _resolve_oauth_provider(provider)
|
||||
|
||||
handler = _LOGOUT_HANDLERS.get(spec.name)
|
||||
if not handler:
|
||||
console.print(f"[red]Logout not implemented for {spec.label}[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
console.print(f"{__logo__} OAuth Logout - {spec.label}\n")
|
||||
handler()
|
||||
|
||||
|
||||
@_register_login("openai_codex")
|
||||
def _login_openai_codex() -> None:
|
||||
try:
|
||||
@ -1544,6 +1576,33 @@ def _login_openai_codex() -> None:
|
||||
raise typer.Exit(1)
|
||||
|
||||
|
||||
@_register_logout("openai_codex")
|
||||
def _logout_openai_codex() -> None:
|
||||
"""清理 OpenAI Codex 的本地 OAuth 凭证。"""
|
||||
try:
|
||||
from oauth_cli_kit.providers import OPENAI_CODEX_PROVIDER
|
||||
from oauth_cli_kit.storage import FileTokenStorage
|
||||
except ImportError:
|
||||
console.print("[red]oauth_cli_kit not installed. Run: pip install oauth-cli-kit[/red]")
|
||||
raise typer.Exit(1)
|
||||
|
||||
storage = FileTokenStorage(token_filename=OPENAI_CODEX_PROVIDER.token_filename)
|
||||
removed_paths: list[Path] = []
|
||||
|
||||
for path in (storage.get_token_path(), storage.get_token_path().with_suffix(".lock")):
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
removed_paths.append(path)
|
||||
|
||||
if not removed_paths:
|
||||
console.print("[yellow]! No local OAuth credentials found for OpenAI Codex[/yellow]")
|
||||
return
|
||||
|
||||
console.print("[green]✓ Logged out from OpenAI Codex[/green]")
|
||||
for path in removed_paths:
|
||||
console.print(f"[dim]Removed: {path}[/dim]")
|
||||
|
||||
|
||||
@_register_login("github_copilot")
|
||||
def _login_github_copilot() -> None:
|
||||
try:
|
||||
|
||||
@ -220,6 +220,32 @@ def test_config_dump_excludes_oauth_provider_blocks():
|
||||
assert "githubCopilot" not in providers
|
||||
|
||||
|
||||
def test_provider_logout_openai_codex_removes_local_oauth_files(tmp_path, monkeypatch):
|
||||
token_path = tmp_path / "auth" / "oauth.json"
|
||||
lock_path = token_path.with_suffix(".lock")
|
||||
token_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
token_path.write_text("{}", encoding="utf-8")
|
||||
lock_path.write_text("", encoding="utf-8")
|
||||
monkeypatch.setenv("OAUTH_CLI_KIT_TOKEN_PATH", str(token_path))
|
||||
|
||||
result = runner.invoke(app, ["provider", "logout", "openai-codex"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert not token_path.exists()
|
||||
assert not lock_path.exists()
|
||||
assert "Logged out from OpenAI Codex" in result.stdout
|
||||
|
||||
|
||||
def test_provider_logout_openai_codex_succeeds_when_no_local_oauth_file(monkeypatch, tmp_path):
|
||||
token_path = tmp_path / "auth" / "oauth.json"
|
||||
monkeypatch.setenv("OAUTH_CLI_KIT_TOKEN_PATH", str(token_path))
|
||||
|
||||
result = runner.invoke(app, ["provider", "logout", "openai-codex"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "No local OAuth credentials found for OpenAI Codex" in result.stdout
|
||||
|
||||
|
||||
def test_config_matches_explicit_ollama_prefix_without_api_key():
|
||||
config = Config()
|
||||
config.agents.defaults.model = "ollama/llama3.2"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user