Merge origin/main into bb/desktop-profile-support
Resolve conflicts in desktop settings/cron/messaging/sidebar: adopt main's ListRow + actions-menu refactors for credential rows; keep our profileColor import on the sidebar. Drop the now-orphaned Tip-based helpers.
This commit is contained in:
+465
-47
@@ -14,11 +14,14 @@ from contextlib import asynccontextmanager
|
||||
import asyncio
|
||||
import base64
|
||||
import binascii
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
import hmac
|
||||
import importlib.util
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import stat
|
||||
import subprocess
|
||||
@@ -26,6 +29,7 @@ import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
@@ -557,6 +561,14 @@ class MessagingPlatformUpdate(BaseModel):
|
||||
clear_env: List[str] = []
|
||||
|
||||
|
||||
class TelegramOnboardingStart(BaseModel):
|
||||
bot_name: Optional[str] = None
|
||||
|
||||
|
||||
class TelegramOnboardingApply(BaseModel):
|
||||
allowed_user_ids: List[str]
|
||||
|
||||
|
||||
class AudioTranscriptionRequest(BaseModel):
|
||||
data_url: str
|
||||
mime_type: Optional[str] = None
|
||||
@@ -1720,14 +1732,15 @@ async def get_profiles_sessions(
|
||||
|
||||
@app.get("/api/sessions/search")
|
||||
async def search_sessions(q: str = "", limit: int = 20):
|
||||
"""Full-text search across session message content using FTS5.
|
||||
"""Search sessions by ID plus full-text message content using FTS5.
|
||||
|
||||
Results are deduped by compression lineage, not by raw ``session_id``.
|
||||
Auto-compression rotates a conversation onto a fresh session id (and leaves
|
||||
the old segment's messages in the FTS index), so one logical chat can own
|
||||
many ``sessions`` rows that all match the same query. Branches also use
|
||||
``parent_session_id``, but they are real alternate conversations; don't
|
||||
collapse branch-specific hits back into the parent.
|
||||
Direct session-id matches are surfaced first, then FTS message-content
|
||||
matches. Results are deduped by compression lineage, not by raw
|
||||
``session_id``. Auto-compression rotates a conversation onto a fresh
|
||||
session id (and leaves the old segment's messages in the FTS index), so one
|
||||
logical chat can own many ``sessions`` rows that all match the same query.
|
||||
Branches also use ``parent_session_id``, but they are real alternate
|
||||
conversations; don't collapse branch-specific hits back into the parent.
|
||||
"""
|
||||
if not q or not q.strip():
|
||||
return {"results": []}
|
||||
@@ -1735,21 +1748,7 @@ async def search_sessions(q: str = "", limit: int = 20):
|
||||
from hermes_state import SessionDB
|
||||
db = SessionDB()
|
||||
try:
|
||||
# Auto-add prefix wildcards so partial words match
|
||||
# e.g. "nimb" → "nimb*" matches "nimby"
|
||||
# Preserve quoted phrases and existing wildcards as-is
|
||||
import re
|
||||
terms = []
|
||||
for token in re.findall(r'"[^"]*"|\S+', q.strip()):
|
||||
if token.startswith('"') or token.endswith("*"):
|
||||
terms.append(token)
|
||||
else:
|
||||
terms.append(token + "*")
|
||||
prefix_query = " ".join(terms)
|
||||
# Over-fetch so lineage dedup can still surface `limit` distinct
|
||||
# conversations even when several hits collapse onto one root.
|
||||
fetch_limit = max(limit * 5, 50)
|
||||
matches = db.search_messages(query=prefix_query, limit=fetch_limit)
|
||||
safe_limit = max(1, min(int(limit or 20), 100))
|
||||
|
||||
# Walk parent_session_id to the compression root, memoized so a
|
||||
# chain of compression segments only costs one walk. We deliberately
|
||||
@@ -1821,24 +1820,71 @@ async def search_sessions(q: str = "", limit: int = 20):
|
||||
tip_cache[root_id] = tip
|
||||
return tip
|
||||
|
||||
# Keep the best (first / most relevant) hit per compression root.
|
||||
# Both ID matches and content matches share one keyspace, keyed by
|
||||
# compression lineage root, so an id-hit and a content-hit on the
|
||||
# same logical conversation collapse to a single result. The first
|
||||
# hit for a lineage wins; ID matches run first and take priority.
|
||||
seen: dict = {}
|
||||
for m in matches:
|
||||
raw_sid = m["session_id"]
|
||||
|
||||
def add_lineage_result(raw_sid: str, payload: dict) -> None:
|
||||
if not raw_sid:
|
||||
return
|
||||
root = compression_root(raw_sid)
|
||||
if root in seen:
|
||||
continue
|
||||
seen[root] = {
|
||||
"session_id": lineage_tip(root),
|
||||
"lineage_root": root,
|
||||
"snippet": m.get("snippet", ""),
|
||||
"role": m.get("role"),
|
||||
"source": m.get("source"),
|
||||
"model": m.get("model"),
|
||||
"session_started": m.get("session_started"),
|
||||
}
|
||||
if len(seen) >= limit:
|
||||
if root in seen or len(seen) >= safe_limit:
|
||||
return
|
||||
payload = dict(payload)
|
||||
payload["session_id"] = lineage_tip(root)
|
||||
payload["lineage_root"] = root
|
||||
seen[root] = payload
|
||||
|
||||
# Direct ID matches first: users often paste a session id from CLI,
|
||||
# logs, or another Hermes surface. FTS can't find those unless the
|
||||
# id happens to appear in message text. search_sessions_by_id is
|
||||
# SQL-bounded, so this stays cheap even with thousands of sessions.
|
||||
for row in db.search_sessions_by_id(q, limit=safe_limit, include_archived=True):
|
||||
sid = row.get("id")
|
||||
preview = (row.get("preview") or "").strip()
|
||||
snippet = preview or f"Session ID: {sid}"
|
||||
add_lineage_result(
|
||||
sid,
|
||||
{
|
||||
"snippet": snippet,
|
||||
"role": None,
|
||||
"source": row.get("source"),
|
||||
"model": row.get("model"),
|
||||
"session_started": row.get("started_at"),
|
||||
},
|
||||
)
|
||||
|
||||
# Auto-add prefix wildcards so partial words match
|
||||
# e.g. "nimb" → "nimb*" matches "nimby"
|
||||
# Preserve quoted phrases and existing wildcards as-is
|
||||
import re
|
||||
terms = []
|
||||
for token in re.findall(r'"[^"]*"|\S+', q.strip()):
|
||||
if token.startswith('"') or token.endswith("*"):
|
||||
terms.append(token)
|
||||
else:
|
||||
terms.append(token + "*")
|
||||
prefix_query = " ".join(terms)
|
||||
# Over-fetch so lineage dedup can still surface `limit` distinct
|
||||
# conversations even when several hits collapse onto one root.
|
||||
fetch_limit = max(safe_limit * 5, 50)
|
||||
matches = db.search_messages(query=prefix_query, limit=fetch_limit)
|
||||
|
||||
for m in matches:
|
||||
if len(seen) >= safe_limit:
|
||||
break
|
||||
add_lineage_result(
|
||||
m["session_id"],
|
||||
{
|
||||
"snippet": m.get("snippet", ""),
|
||||
"role": m.get("role"),
|
||||
"source": m.get("source"),
|
||||
"model": m.get("model"),
|
||||
"session_started": m.get("session_started"),
|
||||
},
|
||||
)
|
||||
return {"results": list(seen.values())}
|
||||
finally:
|
||||
db.close()
|
||||
@@ -2908,18 +2954,66 @@ def _channel_managed_env_keys() -> frozenset[str]:
|
||||
return frozenset()
|
||||
|
||||
|
||||
# Cross-cutting gateway / relay knobs stay on the Keys → Settings tab even though
|
||||
# they use the ``messaging`` category in OPTIONAL_ENV_VARS. Platform-scoped vars
|
||||
# (``DISCORD_*``, ``MATRIX_*``, …) are owned by the Messaging UI instead.
|
||||
_MESSAGING_KEYS_PAGE_KEYS = frozenset({
|
||||
"GATEWAY_ALLOW_ALL_USERS",
|
||||
"GATEWAY_PROXY_KEY",
|
||||
"GATEWAY_PROXY_URL",
|
||||
})
|
||||
|
||||
|
||||
def _platform_env_prefixes(platform_id: str) -> tuple[str, ...]:
|
||||
"""Env-var prefixes owned by a messaging platform card."""
|
||||
aliases: dict[str, tuple[str, ...]] = {
|
||||
"email": ("EMAIL_",),
|
||||
"homeassistant": ("HASS_",),
|
||||
"qqbot": ("QQ_", "QQBOT_"),
|
||||
"sms": ("TWILIO_",),
|
||||
"wecom": ("WECOM_BOT_", "WECOM_SECRET"),
|
||||
"wecom_callback": ("WECOM_CALLBACK_",),
|
||||
}
|
||||
if platform_id in aliases:
|
||||
return aliases[platform_id]
|
||||
return (platform_id.upper().replace("-", "_") + "_",)
|
||||
|
||||
|
||||
def _discover_platform_env_vars(platform_id: str) -> tuple[str, ...]:
|
||||
"""All messaging-category env vars for a platform (override + plugin + prefix)."""
|
||||
prefixes = _platform_env_prefixes(platform_id)
|
||||
keys: list[str] = []
|
||||
for name, info in OPTIONAL_ENV_VARS.items():
|
||||
if info.get("category") != "messaging":
|
||||
continue
|
||||
if name in _MESSAGING_KEYS_PAGE_KEYS:
|
||||
continue
|
||||
if not any(name.startswith(prefix) for prefix in prefixes):
|
||||
continue
|
||||
keys.append(name)
|
||||
return tuple(sorted(set(keys)))
|
||||
|
||||
|
||||
def _merge_platform_env_vars(
|
||||
platform_id: str,
|
||||
override: dict[str, Any],
|
||||
plugin_entry: Any | None,
|
||||
) -> tuple[str, ...]:
|
||||
"""Canonical env-var list for a messaging platform card."""
|
||||
discovered = _discover_platform_env_vars(platform_id)
|
||||
if "env_vars" in override:
|
||||
return tuple(dict.fromkeys((*override["env_vars"], *discovered)))
|
||||
if plugin_entry is not None and plugin_entry.required_env:
|
||||
return tuple(dict.fromkeys((*tuple(plugin_entry.required_env), *discovered)))
|
||||
return discovered
|
||||
|
||||
|
||||
def _build_catalog_entry(
|
||||
platform_id: str, plugin_entry: Any | None = None
|
||||
) -> dict[str, Any]:
|
||||
override = _PLATFORM_OVERRIDES.get(platform_id, {})
|
||||
|
||||
if "env_vars" in override:
|
||||
env_vars: tuple[str, ...] = tuple(override["env_vars"])
|
||||
elif plugin_entry is not None and plugin_entry.required_env:
|
||||
env_vars = tuple(plugin_entry.required_env)
|
||||
else:
|
||||
prefix = platform_id.upper() + "_"
|
||||
env_vars = tuple(k for k in OPTIONAL_ENV_VARS if k.startswith(prefix))
|
||||
env_vars = _merge_platform_env_vars(platform_id, override, plugin_entry)
|
||||
|
||||
if "required_env" in override:
|
||||
required_env = tuple(override["required_env"])
|
||||
@@ -3077,6 +3171,329 @@ def _write_platform_enabled(platform_id: str, enabled: bool) -> None:
|
||||
save_config(config)
|
||||
|
||||
|
||||
_TELEGRAM_ONBOARDING_DEFAULT_URL = "https://setup.hermes-agent.nousresearch.com"
|
||||
_TELEGRAM_USER_ID_RE = re.compile(r"^\d+$")
|
||||
|
||||
|
||||
@dataclass
|
||||
class _TelegramOnboardingPairing:
|
||||
poll_token: str
|
||||
expires_at: str
|
||||
expires_at_ts: float
|
||||
bot_token: str | None = None
|
||||
bot_username: str | None = None
|
||||
owner_user_id: str | None = None
|
||||
|
||||
|
||||
_telegram_onboarding_pairings: dict[str, _TelegramOnboardingPairing] = {}
|
||||
_telegram_onboarding_lock = threading.RLock()
|
||||
|
||||
|
||||
def _telegram_onboarding_base_url() -> str:
|
||||
return (
|
||||
os.getenv("TELEGRAM_ONBOARDING_URL", _TELEGRAM_ONBOARDING_DEFAULT_URL)
|
||||
.strip()
|
||||
.rstrip("/")
|
||||
)
|
||||
|
||||
|
||||
def _parse_expiry_ts(value: str) -> float:
|
||||
try:
|
||||
normalized = value.replace("Z", "+00:00")
|
||||
parsed = datetime.fromisoformat(normalized)
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.replace(tzinfo=timezone.utc)
|
||||
return parsed.timestamp()
|
||||
except Exception:
|
||||
return time.time() + 600
|
||||
|
||||
|
||||
def _prune_telegram_onboarding_pairings() -> None:
|
||||
now = time.time()
|
||||
expired = [
|
||||
pairing_id
|
||||
for pairing_id, record in _telegram_onboarding_pairings.items()
|
||||
if record.expires_at_ts <= now
|
||||
]
|
||||
for pairing_id in expired:
|
||||
_telegram_onboarding_pairings.pop(pairing_id, None)
|
||||
|
||||
|
||||
def _normalize_telegram_user_id(value: Any) -> str | None:
|
||||
normalized = str(value or "").strip()
|
||||
if _TELEGRAM_USER_ID_RE.fullmatch(normalized):
|
||||
return normalized
|
||||
return None
|
||||
|
||||
|
||||
def _telegram_onboarding_error_message(error: str, fallback: str) -> str:
|
||||
return {
|
||||
"not_found": "Telegram pairing was not found. Start a new setup.",
|
||||
"expired": "Telegram setup expired. Start a new setup.",
|
||||
"claimed": "Telegram setup was already claimed. Start a new setup.",
|
||||
"unauthorized": "Telegram setup service rejected this request.",
|
||||
"telegram_manager_bot_token_not_configured": "Telegram setup service is not configured.",
|
||||
"telegram_token_fetch_failed": "Telegram could not finish bot setup. Try again.",
|
||||
}.get(error, fallback)
|
||||
|
||||
|
||||
def _telegram_onboarding_request_sync(
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
body: dict[str, Any] | None = None,
|
||||
bearer_token: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
data = None
|
||||
headers = {"Accept": "application/json"}
|
||||
if body is not None:
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
headers["Content-Type"] = "application/json"
|
||||
if bearer_token:
|
||||
headers["Authorization"] = f"Bearer {bearer_token}"
|
||||
|
||||
request = urllib.request.Request(
|
||||
f"{_telegram_onboarding_base_url()}{path}",
|
||||
data=data,
|
||||
headers=headers,
|
||||
method=method,
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=10) as response:
|
||||
payload = response.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
payload = exc.read()
|
||||
try:
|
||||
parsed = json.loads(payload.decode("utf-8"))
|
||||
except Exception:
|
||||
parsed = {}
|
||||
error = str(parsed.get("error") or parsed.get("status") or "")
|
||||
detail = _telegram_onboarding_error_message(
|
||||
error,
|
||||
"Telegram setup service returned an error.",
|
||||
)
|
||||
status_code = 404 if exc.code == 404 else 502
|
||||
if error in {"expired", "claimed"}:
|
||||
status_code = 410
|
||||
raise HTTPException(status_code=status_code, detail=detail) from exc
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Telegram setup service is unavailable. Try again shortly.",
|
||||
) from exc
|
||||
|
||||
try:
|
||||
parsed = json.loads(payload.decode("utf-8"))
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Telegram setup service returned an invalid response.",
|
||||
) from exc
|
||||
if not isinstance(parsed, dict):
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Telegram setup service returned an invalid response.",
|
||||
)
|
||||
return parsed
|
||||
|
||||
|
||||
async def _telegram_onboarding_request(
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
body: dict[str, Any] | None = None,
|
||||
bearer_token: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return await asyncio.to_thread(
|
||||
_telegram_onboarding_request_sync,
|
||||
method,
|
||||
path,
|
||||
body=body,
|
||||
bearer_token=bearer_token,
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/messaging/telegram/onboarding/start")
|
||||
async def start_telegram_onboarding(body: TelegramOnboardingStart):
|
||||
bot_name = (body.bot_name or "Hermes Agent").strip() or "Hermes Agent"
|
||||
payload = await _telegram_onboarding_request(
|
||||
"POST",
|
||||
"/v1/telegram/pairings",
|
||||
body={"bot_name": bot_name},
|
||||
)
|
||||
|
||||
pairing_id = str(payload.get("pairing_id") or "").strip()
|
||||
poll_token = str(payload.get("poll_token") or "").strip()
|
||||
expires_at = str(payload.get("expires_at") or "").strip()
|
||||
deep_link = str(payload.get("deep_link") or "").strip()
|
||||
qr_payload = str(payload.get("qr_payload") or deep_link).strip()
|
||||
suggested_username = str(payload.get("suggested_username") or "").strip()
|
||||
if not pairing_id or not poll_token or not expires_at or not deep_link:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Telegram setup service returned an incomplete response.",
|
||||
)
|
||||
|
||||
with _telegram_onboarding_lock:
|
||||
_prune_telegram_onboarding_pairings()
|
||||
_telegram_onboarding_pairings[pairing_id] = _TelegramOnboardingPairing(
|
||||
poll_token=poll_token,
|
||||
expires_at=expires_at,
|
||||
expires_at_ts=_parse_expiry_ts(expires_at),
|
||||
)
|
||||
|
||||
return {
|
||||
"pairing_id": pairing_id,
|
||||
"suggested_username": suggested_username,
|
||||
"deep_link": deep_link,
|
||||
"qr_payload": qr_payload,
|
||||
"expires_at": expires_at,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/messaging/telegram/onboarding/{pairing_id}")
|
||||
async def get_telegram_onboarding_status(pairing_id: str):
|
||||
with _telegram_onboarding_lock:
|
||||
_prune_telegram_onboarding_pairings()
|
||||
record = _telegram_onboarding_pairings.get(pairing_id)
|
||||
if not record:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Telegram setup session was not found. Start a new setup.",
|
||||
)
|
||||
if record.bot_token:
|
||||
return {
|
||||
"status": "ready",
|
||||
"bot_username": record.bot_username,
|
||||
"owner_user_id": record.owner_user_id,
|
||||
"expires_at": record.expires_at,
|
||||
}
|
||||
poll_token = record.poll_token
|
||||
|
||||
payload = await _telegram_onboarding_request(
|
||||
"GET",
|
||||
f"/v1/telegram/pairings/{urllib.parse.quote(pairing_id, safe='')}",
|
||||
bearer_token=poll_token,
|
||||
)
|
||||
status = str(payload.get("status") or "").strip()
|
||||
if status == "waiting":
|
||||
with _telegram_onboarding_lock:
|
||||
current = _telegram_onboarding_pairings.get(pairing_id)
|
||||
expires_at = current.expires_at if current else ""
|
||||
return {"status": "waiting", "expires_at": expires_at}
|
||||
|
||||
if status == "ready":
|
||||
bot_token = str(payload.get("token") or "").strip()
|
||||
bot_username = str(payload.get("bot_username") or "").strip()
|
||||
if not bot_token:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Telegram setup service returned an incomplete response.",
|
||||
)
|
||||
owner_user_id = _normalize_telegram_user_id(payload.get("owner_user_id"))
|
||||
with _telegram_onboarding_lock:
|
||||
record = _telegram_onboarding_pairings.get(pairing_id)
|
||||
if not record:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Telegram setup session was not found. Start a new setup.",
|
||||
)
|
||||
record.bot_token = bot_token
|
||||
record.bot_username = bot_username or None
|
||||
record.owner_user_id = owner_user_id
|
||||
return {
|
||||
"status": "ready",
|
||||
"bot_username": record.bot_username,
|
||||
"owner_user_id": record.owner_user_id,
|
||||
"expires_at": record.expires_at,
|
||||
}
|
||||
|
||||
if status in {"expired", "claimed"}:
|
||||
with _telegram_onboarding_lock:
|
||||
_telegram_onboarding_pairings.pop(pairing_id, None)
|
||||
raise HTTPException(
|
||||
status_code=410,
|
||||
detail=_telegram_onboarding_error_message(
|
||||
status,
|
||||
"Telegram setup is no longer available. Start a new setup.",
|
||||
),
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Telegram setup service returned an unknown status.",
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/messaging/telegram/onboarding/{pairing_id}/apply")
|
||||
async def apply_telegram_onboarding(
|
||||
pairing_id: str, body: TelegramOnboardingApply
|
||||
):
|
||||
allowed_user_ids = []
|
||||
seen = set()
|
||||
for raw_id in body.allowed_user_ids:
|
||||
normalized = _normalize_telegram_user_id(raw_id)
|
||||
if not normalized:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Allowed Telegram user IDs must be numeric.",
|
||||
)
|
||||
if normalized not in seen:
|
||||
seen.add(normalized)
|
||||
allowed_user_ids.append(normalized)
|
||||
if not allowed_user_ids:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Add at least one allowed Telegram user ID.",
|
||||
)
|
||||
|
||||
with _telegram_onboarding_lock:
|
||||
_prune_telegram_onboarding_pairings()
|
||||
record = _telegram_onboarding_pairings.get(pairing_id)
|
||||
if not record:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Telegram setup session was not found. Start a new setup.",
|
||||
)
|
||||
bot_token = record.bot_token
|
||||
bot_username = record.bot_username
|
||||
if not bot_token:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="Telegram setup is not ready yet.",
|
||||
)
|
||||
|
||||
try:
|
||||
save_env_value("TELEGRAM_BOT_TOKEN", bot_token)
|
||||
save_env_value("TELEGRAM_ALLOWED_USERS", ",".join(allowed_user_ids))
|
||||
_write_platform_enabled("telegram", True)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except Exception as exc:
|
||||
_log.exception("Telegram onboarding apply failed")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Failed to save Telegram setup.",
|
||||
) from exc
|
||||
|
||||
with _telegram_onboarding_lock:
|
||||
_telegram_onboarding_pairings.pop(pairing_id, None)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"platform": "telegram",
|
||||
"bot_username": bot_username,
|
||||
"needs_restart": True,
|
||||
}
|
||||
|
||||
|
||||
@app.delete("/api/messaging/telegram/onboarding/{pairing_id}")
|
||||
async def cancel_telegram_onboarding(pairing_id: str):
|
||||
with _telegram_onboarding_lock:
|
||||
_telegram_onboarding_pairings.pop(pairing_id, None)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/api/messaging/platforms")
|
||||
async def get_messaging_platforms():
|
||||
env_on_disk = load_env()
|
||||
@@ -6771,6 +7188,7 @@ async def get_toolsets():
|
||||
_get_effective_configurable_toolsets,
|
||||
_get_platform_tools,
|
||||
_toolset_has_keys,
|
||||
gui_toolset_label,
|
||||
)
|
||||
from toolsets import resolve_toolset
|
||||
|
||||
@@ -6788,7 +7206,9 @@ async def get_toolsets():
|
||||
tools = []
|
||||
is_enabled = name in enabled_toolsets
|
||||
result.append({
|
||||
"name": name, "label": label, "description": desc,
|
||||
"name": name,
|
||||
"label": gui_toolset_label(label),
|
||||
"description": desc,
|
||||
"enabled": is_enabled,
|
||||
"available": is_enabled,
|
||||
"configured": _toolset_has_keys(name, config),
|
||||
@@ -7135,8 +7555,6 @@ async def get_models_analytics(days: int = 30):
|
||||
# though uvicorn binds to 127.0.0.1.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
import re
|
||||
|
||||
# PTY bridge is POSIX-only (depends on fcntl/termios/ptyprocess). On native
|
||||
# Windows the import raises; catch and leave PtyBridge=None so the rest of
|
||||
# the dashboard (sessions, jobs, metrics, config editor) still loads and the
|
||||
|
||||
Reference in New Issue
Block a user