fix(xiaohe): completion 缺失 _xiaohe + 首次运行无认证时引导设置 provider

- completion_install: completions_installed() 原只检查 progs[0](myagents),
  已存在则短路,导致 xiaohe 等后加 prog 的 completion 从未安装。
  改为遍历全部 prog,任一缺失即触发重新安装。
- first_run.py: xiaohe_main() 启动时检测是否有任何可用认证(env var、
  credentials、settings.json、已存 keys),无则弹交互式 provider 选择菜单。
- install.sh.in: 移除末尾 xiaohe init + scripts/init.sh wizard,安装脚本
  只装软件不初始化配置。
This commit is contained in:
Zhengshou Lai
2026-07-16 23:25:04 +08:00
parent 3d840a1ca3
commit 12d746e0f8
3 changed files with 187 additions and 10 deletions
+12 -10
View File
@@ -132,16 +132,18 @@ def completions_installed(
*,
install_root: Path | None = None,
) -> bool:
"""Check whether the zsh completion for the first prog is current."""
if not progs:
return True
prog_name, _ = progs[0]
for target in completion_targets(prog_name, install_root=install_root):
if target.shell == "zsh" and target.installed:
marker = f"_{prog_name.upper()}_COMPLETE"
if marker in target.path.read_text(encoding="utf-8"):
return True
return False
"""Check whether the zsh completion for every prog is current."""
for prog_name, _ in progs:
ok = False
for target in completion_targets(prog_name, install_root=install_root):
if target.shell == "zsh" and target.installed:
marker = f"_{prog_name.upper()}_COMPLETE"
if marker in target.path.read_text(encoding="utf-8"):
ok = True
break
if not ok:
return False
return bool(progs)
def ensure_completions_installed(
+173
View File
@@ -0,0 +1,173 @@
"""First-run detection: guide user through provider setup when nothing is configured.
Called from ``xiaohe_main()`` on every invocation. When no auth is detected
(no env vars, no stored keys, no login credentials), prints a menu and
walks the user through selecting a provider and entering an API key.
"""
from __future__ import annotations
import os
from pathlib import Path
import click
from rich.console import Console
from myagents.backends import (
PROVIDERS,
THIRD_PARTY_PROVIDER_IDS,
Provider,
)
from myagents.claude_settings import (
apply_provider,
get_active_provider,
load_settings,
save_settings,
)
from myagents.secrets import has_key, set_key
stderr_console = Console(stderr=True)
def _has_claude_login() -> bool:
"""Check for Claude Code OAuth login credentials file."""
creds = Path.home() / ".claude" / ".credentials.json"
return creds.is_file()
def is_any_auth_configured() -> bool:
"""Return True when at least one usable auth method is available."""
# 1. Official Claude: env var or login credentials file
if os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("ANTHROPIC_AUTH_TOKEN"):
return True
if _has_claude_login():
return True
# 2. Third-party provider active in Claude Code settings
if get_active_provider() is not None:
return True
# 3. ANTHROPIC_API_KEY set in ~/.claude/settings.json env block
env = load_settings().get("env", {})
if isinstance(env, dict) and env.get("ANTHROPIC_API_KEY"):
return True
# 4. Stored third-party keys (even if not currently active)
for pid in THIRD_PARTY_PROVIDER_IDS:
if has_key(PROVIDERS[pid].key_name):
return True
return False
def _secure_prompt_key(provider: Provider) -> str:
"""Prompt for an API key with hidden input."""
try:
return click.prompt(
f"Enter {provider.display_name} API key",
hide_input=True,
err=True,
)
except click.UsageError:
try:
with open("/dev/tty", encoding="utf-8") as tty:
return tty.readline().rstrip("\n")
except OSError as exc:
raise click.ClickException(
f"Cannot read API key interactively: {exc}"
) from exc
def _persist_claude_api_key(key: str) -> None:
"""Write ANTHROPIC_API_KEY to ~/.claude/settings.json env block."""
settings = load_settings()
env: dict = {k: v for k, v in settings.get("env", {}).items()
if isinstance(v, str)}
env["ANTHROPIC_API_KEY"] = key
settings["env"] = env
save_settings(settings)
def _run_setup_wizard() -> None:
"""Interactive guided setup: choose provider and enter API key."""
providers = [
PROVIDERS["claude"],
PROVIDERS["deepseek"],
PROVIDERS["kimi"],
PROVIDERS["kimi-code"],
]
stderr_console.print(
"\n[bold yellow]No model provider configured yet.[/bold yellow]"
)
stderr_console.print(
"[dim]Claude Code needs authentication. Pick one:[/dim]\n"
)
for i, p in enumerate(providers, 1):
stderr_console.print(f" {i}) {p.display_name}")
stderr_console.print(
" [dim]Or press Enter to skip (set up later with:"
" xiaohe switch provider <name>)[/dim]"
)
try:
choice = click.prompt(
"Choose", default="", show_default=False, err=True
)
except (click.Abort, EOFError):
stderr_console.print("[dim]Skipped. Run 'xiaohe switch provider' later.[/dim]")
return
choice = choice.strip()
if not choice:
stderr_console.print("[dim]Skipped. Run 'xiaohe switch provider' later.[/dim]")
return
try:
idx = int(choice) - 1
if idx < 0 or idx >= len(providers):
raise ValueError
except ValueError:
stderr_console.print(f"[red]Invalid choice: {choice}[/red]")
return
provider = providers[idx]
stderr_console.print(f"\n[bold]{provider.display_name}[/bold] selected.")
if provider.id == "claude":
key = _secure_prompt_key(provider)
if not key:
stderr_console.print("[yellow]No key entered — skipped.[/yellow]")
return
_persist_claude_api_key(key)
stderr_console.print(
f"[green]{provider.display_name} API key saved to"
f" ~/.claude/settings.json.[/green]"
)
else:
key = _secure_prompt_key(provider)
if not key:
stderr_console.print("[yellow]No key entered — skipped.[/yellow]")
return
set_key(provider.key_name, key)
settings = apply_provider(provider, key=key)
save_settings(settings)
stderr_console.print(
f"[green]Switched provider to {provider.display_name}.[/green]"
)
stderr_console.print()
def ensure_auth_configured() -> None:
"""Check auth state on ``xiaohe`` startup; run wizard if unconfigured."""
if is_any_auth_configured():
return
try:
_run_setup_wizard()
except Exception:
# Never block xiaohe launch on a setup error.
stderr_console.print(
"[yellow]Setup interrupted."
" Run 'xiaohe switch provider' later to configure.[/yellow]"
)
+2
View File
@@ -110,6 +110,8 @@ def xiaohe_main() -> None:
from myagents.commands.completion_install import (
ensure_completions_installed,
)
from myagents.commands.first_run import ensure_auth_configured
ensure_completions_installed(_progs())
ensure_auth_configured()
build_xiaohe_cli()()