Merge branch 'main' into pr-630

This commit is contained in:
Re-bin 2026-02-18 03:13:00 +00:00
commit df09ba1232
4 changed files with 75 additions and 33 deletions

View File

@ -588,7 +588,7 @@ Config file: `~/.nanobot/config.json`
| `zhipu` | LLM (Zhipu GLM) | [open.bigmodel.cn](https://open.bigmodel.cn) | | `zhipu` | LLM (Zhipu GLM) | [open.bigmodel.cn](https://open.bigmodel.cn) |
| `vllm` | LLM (local, any OpenAI-compatible server) | — | | `vllm` | LLM (local, any OpenAI-compatible server) | — |
| `openai_codex` | LLM (Codex, OAuth) | `nanobot provider login openai-codex` | | `openai_codex` | LLM (Codex, OAuth) | `nanobot provider login openai-codex` |
| `github_copilot` | LLM (GitHub Copilot, OAuth) | Requires [GitHub Copilot](https://github.com/features/copilot) subscription | | `github_copilot` | LLM (GitHub Copilot, OAuth) | `nanobot provider login github-copilot` |
<details> <details>
<summary><b>OpenAI Codex (OAuth)</b></summary> <summary><b>OpenAI Codex (OAuth)</b></summary>

View File

@ -152,13 +152,15 @@ class SlackChannel(BaseChannel):
text = self._strip_bot_mention(text) text = self._strip_bot_mention(text)
thread_ts = event.get("thread_ts") or event.get("ts") thread_ts = event.get("thread_ts")
if self.config.reply_in_thread and not thread_ts:
thread_ts = event.get("ts")
# Add :eyes: reaction to the triggering message (best-effort) # Add :eyes: reaction to the triggering message (best-effort)
try: try:
if self._web_client and event.get("ts"): if self._web_client and event.get("ts"):
await self._web_client.reactions_add( await self._web_client.reactions_add(
channel=chat_id, channel=chat_id,
name="eyes", name=self.config.react_emoji,
timestamp=event.get("ts"), timestamp=event.get("ts"),
) )
except Exception as e: except Exception as e:

View File

@ -901,7 +901,9 @@ def status():
p = getattr(config.providers, spec.name, None) p = getattr(config.providers, spec.name, None)
if p is None: if p is None:
continue continue
if spec.is_local: if spec.is_oauth:
console.print(f"{spec.label}: [green]✓ (OAuth)[/green]")
elif spec.is_local:
# Local deployments show api_base instead of api_key # Local deployments show api_base instead of api_key
if p.api_base: if p.api_base:
console.print(f"{spec.label}: [green]✓ {p.api_base}[/green]") console.print(f"{spec.label}: [green]✓ {p.api_base}[/green]")
@ -920,42 +922,78 @@ provider_app = typer.Typer(help="Manage providers")
app.add_typer(provider_app, name="provider") app.add_typer(provider_app, name="provider")
_LOGIN_HANDLERS: dict[str, callable] = {}
def _register_login(name: str):
def decorator(fn):
_LOGIN_HANDLERS[name] = fn
return fn
return decorator
@provider_app.command("login") @provider_app.command("login")
def provider_login( def provider_login(
provider: str = typer.Argument(..., help="OAuth provider to authenticate with (e.g., 'openai-codex')"), provider: str = typer.Argument(..., help="OAuth provider (e.g. 'openai-codex', 'github-copilot')"),
): ):
"""Authenticate with an OAuth provider.""" """Authenticate with an OAuth provider."""
console.print(f"{__logo__} OAuth Login - {provider}\n") from nanobot.providers.registry import PROVIDERS
if provider == "openai-codex": key = provider.replace("-", "_")
spec = next((s for s in PROVIDERS if s.name == key and s.is_oauth), None)
if not spec:
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)
handler = _LOGIN_HANDLERS.get(spec.name)
if not handler:
console.print(f"[red]Login not implemented for {spec.label}[/red]")
raise typer.Exit(1)
console.print(f"{__logo__} OAuth Login - {spec.label}\n")
handler()
@_register_login("openai_codex")
def _login_openai_codex() -> None:
try:
from oauth_cli_kit import get_token, login_oauth_interactive
token = None
try: try:
from oauth_cli_kit import get_token, login_oauth_interactive token = get_token()
token = None except Exception:
try: pass
token = get_token() if not (token and token.access):
except Exception: console.print("[cyan]Starting interactive OAuth login...[/cyan]\n")
token = None token = login_oauth_interactive(
if not (token and token.access): print_fn=lambda s: console.print(s),
console.print("[cyan]No valid token found. Starting interactive OAuth login...[/cyan]") prompt_fn=lambda s: typer.prompt(s),
console.print("A browser window may open for you to authenticate.\n") )
token = login_oauth_interactive( if not (token and token.access):
print_fn=lambda s: console.print(s), console.print("[red]✗ Authentication failed[/red]")
prompt_fn=lambda s: typer.prompt(s),
)
if not (token and token.access):
console.print("[red]✗ Authentication failed[/red]")
raise typer.Exit(1)
console.print("[green]✓ Successfully authenticated with OpenAI Codex![/green]")
console.print(f"[dim]Account ID: {token.account_id}[/dim]")
except ImportError:
console.print("[red]oauth_cli_kit not installed. Run: pip install oauth-cli-kit[/red]")
raise typer.Exit(1) raise typer.Exit(1)
except Exception as e: console.print(f"[green]✓ Authenticated with OpenAI Codex[/green] [dim]{token.account_id}[/dim]")
console.print(f"[red]Authentication error: {e}[/red]") except ImportError:
raise typer.Exit(1) console.print("[red]oauth_cli_kit not installed. Run: pip install oauth-cli-kit[/red]")
else: raise typer.Exit(1)
console.print(f"[red]Unknown OAuth provider: {provider}[/red]")
console.print("[yellow]Supported providers: openai-codex[/yellow]")
@_register_login("github_copilot")
def _login_github_copilot() -> None:
import asyncio
console.print("[cyan]Starting GitHub Copilot device flow...[/cyan]\n")
async def _trigger():
from litellm import acompletion
await acompletion(model="github_copilot/gpt-4o", messages=[{"role": "user", "content": "hi"}], max_tokens=1)
try:
asyncio.run(_trigger())
console.print("[green]✓ Authenticated with GitHub Copilot[/green]")
except Exception as e:
console.print(f"[red]Authentication error: {e}[/red]")
raise typer.Exit(1) raise typer.Exit(1)

View File

@ -148,6 +148,8 @@ class SlackConfig(Base):
bot_token: str = "" # xoxb-... bot_token: str = "" # xoxb-...
app_token: str = "" # xapp-... app_token: str = "" # xapp-...
user_token_read_only: bool = True user_token_read_only: bool = True
reply_in_thread: bool = True
react_emoji: str = "eyes"
group_policy: str = "mention" # "mention", "open", "allowlist" group_policy: str = "mention" # "mention", "open", "allowlist"
group_allow_from: list[str] = Field(default_factory=list) # Allowed channel IDs if allowlist group_allow_from: list[str] = Field(default_factory=list) # Allowed channel IDs if allowlist
dm: SlackDMConfig = Field(default_factory=SlackDMConfig) dm: SlackDMConfig = Field(default_factory=SlackDMConfig)