fix(integrations): MAX endpoint platform-api.max.ru; bot identity from API

platform-api2.max.ru serves an incomplete cert chain (verify failed) and
botapi.max.ru was the wrong host; use https://platform-api.max.ru (valid cert).

Bot name/ID is not a form input and not needed for requests (the token
identifies the bot). Remove the editable bot-name field; the Проверить action
derives bot id/username/name from the provider (MAX /me, Telegram getMe) and
stores them read-only, shown in the list and edit form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AndreyandClaude Opus 4.8 committed 2026-06-28 01:01:33 +03:00
1 parent bbcc258221
commit 360ebcf8a2
6 files changed
+33 -20

No files matched your search

@@ -21,7 +21,9 @@ import urllib.request
from django.conf import settings
DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
DEFAULT_MAX_BASE_URL = "https://platform-api2.max.ru"
# platform-api2.max.ru отдаёт неполную цепочку сертификата (verify failed);
# рабочий и с валидным сертификатом — platform-api.max.ru.
DEFAULT_MAX_BASE_URL = "https://platform-api.max.ru"
DEFAULT_TELEGRAM_BASE_URL = "https://api.telegram.org"
CheckResult = tuple[bool, str, dict]
@@ -72,9 +74,11 @@ def check_max(*, secret: str, base_url: str) -> CheckResult:
status, data = _get(f"{base}/me", headers={"Authorization": secret})
if status != 200:
return False, f"MAX ответил {status}", {}
bot_id = data.get("user_id")
username = data.get("username") or ""
name = data.get("name") or username or "бот подключён"
return True, f"MAX: {name}", {"bot_username": username or name}
meta = {"bot_id": str(bot_id) if bot_id else "", "bot_username": username, "bot_name": name}
return True, f"MAX: {name}", meta
return _safe(run)
@@ -89,7 +93,12 @@ def check_telegram(*, secret: str, base_url: str) -> CheckResult:
status, data = _get(f"{base}/bot{secret}/getMe")
if status != 200 or not data.get("ok"):
return False, f"Telegram ответил {status}", {}
username = (data.get("result") or {}).get("username") or ""
return True, f"Telegram: @{username}" if username else "Telegram: бот подключён", {"bot_username": username}
result = data.get("result") or {}
bot_id = result.get("id")
username = result.get("username") or ""
name = result.get("first_name") or username or "бот подключён"
meta = {"bot_id": str(bot_id) if bot_id else "", "bot_username": username, "bot_name": name}
detail = f"Telegram: @{username}" if username else "Telegram: бот подключён"
return True, detail, meta
return _safe(run)
@@ -12,7 +12,9 @@ def integration_payload(integration: Integration) -> dict[str, object]:
"config": {
"baseUrl": integration.config.get("base_url", ""),
"defaultModel": integration.config.get("default_model", ""),
"botId": integration.config.get("bot_id", ""),
"botUsername": integration.config.get("bot_username", ""),
"botName": integration.config.get("bot_name", ""),
},
"status": integration.status,
"lastCheckedAt": integration.last_checked_at.isoformat() if integration.last_checked_at else None,
@@ -103,10 +103,14 @@ def test_integration(*, integration: Integration) -> Integration:
integration.last_error = "" if ok else detail
integration.last_checked_at = timezone.now()
update_fields = ["status", "last_error", "last_checked_at", "updated_at"]
# Реальное имя бота из ответа API — авторитетный источник, перезаписываем.
bot_username = meta.get("bot_username") if ok else None
if bot_username:
integration.config = {**integration.config, "bot_username": bot_username}
update_fields.append("config")
# Идентичность бота (id/username/имя) — из ответа API, авторитетный источник.
if ok and meta:
config = {**integration.config}
for key in ("bot_id", "bot_username", "bot_name"):
if meta.get(key):
config[key] = meta[key]
if config != integration.config:
integration.config = config
update_fields.append("config")
integration.save(update_fields=update_fields)
return integration
@@ -15,7 +15,6 @@ export function IntegrationForm({ initial, onClose, onSaved }: { initial: Integr
const [provider, setProvider] = useState<IntegrationProvider>(initial?.provider ?? "OPENROUTER");
const [name, setName] = useState(initial?.name ?? "");
const [secret, setSecret] = useState("");
const [botUsername, setBotUsername] = useState(initial?.config.botUsername ?? "");
const [baseUrl, setBaseUrl] = useState(initial?.config.baseUrl ?? "");
const [defaultModel, setDefaultModel] = useState(initial?.config.defaultModel ?? "");
const [submitting, setSubmitting] = useState(false);
@@ -28,7 +27,7 @@ export function IntegrationForm({ initial, onClose, onSaved }: { initial: Integr
if (!ready) return;
setSubmitting(true);
setError(null);
const config = { baseUrl: baseUrl.trim(), defaultModel: defaultModel.trim(), botUsername: botUsername.trim() };
const config = { baseUrl: baseUrl.trim(), defaultModel: defaultModel.trim() };
try {
if (isEdit) {
await api(`/api/v1/integrations/${initial.id}/`, {
@@ -71,8 +70,8 @@ export function IntegrationForm({ initial, onClose, onSaved }: { initial: Integr
{meta.hasModel && (
<FormField label="Модель по умолчанию" value={defaultModel} onChange={setDefaultModel} placeholder="anthropic/claude-sonnet-4.6" />
)}
{meta.hasBotName && (
<FormField label="Имя бота" value={botUsername} onChange={setBotUsername} placeholder="@username · подставится при проверке" />
{isEdit && initial.config.botUsername && (
<FormField label="Бот" value={`${initial.config.botName || initial.config.botUsername}${initial.config.botUsername ? ` · @${initial.config.botUsername}` : ""}${initial.config.botId ? ` · id ${initial.config.botId}` : ""}`} />
)}
{error && <div className="integration-form-error">{error}</div>}
<div className="integration-form-actions">
@@ -115,7 +115,7 @@ export function IntegrationsPage() {
<td>
<div className="product-cell">
<span className="product-icon"><Icon name={item.kind === "LLM_PROVIDER" ? "robot" : "plug"} size={20} /></span>
<span><strong>{item.name}</strong><small>{meta.label}{item.config.botUsername ? ` · ${item.config.botUsername}` : ""}</small></span>
<span><strong>{item.name}</strong><small>{meta.label}{item.config.botUsername ? ` · @${item.config.botUsername}` : item.config.botName ? ` · ${item.config.botName}` : ""}</small></span>
</div>
</td>
<td>{item.hasSecret ? <code className="ai-mono">••••••••</code> : <span className="product-empty-value">—</span>}</td>
@@ -8,7 +8,7 @@ export type Integration = {
provider: IntegrationProvider;
name: string;
hasSecret: boolean;
config: { baseUrl: string; defaultModel: string; botUsername: string };
config: { baseUrl: string; defaultModel: string; botId: string; botUsername: string; botName: string };
status: IntegrationStatus;
lastCheckedAt: string | null;
lastError: string;
@@ -22,15 +22,14 @@ type ProviderMeta = {
secretLabel: string;
defaultBaseUrl: string;
hasModel: boolean;
hasBotName: boolean;
testable: boolean;
};
export const PROVIDERS: Record<IntegrationProvider, ProviderMeta> = {
OPENROUTER: { label: "OpenRouter", kind: "LLM_PROVIDER", secretLabel: "API-ключ", defaultBaseUrl: "https://openrouter.ai/api/v1", hasModel: true, hasBotName: false, testable: true },
MAX: { label: "MAX", kind: "MESSENGER", secretLabel: "Токен бота", defaultBaseUrl: "https://platform-api2.max.ru", hasModel: false, hasBotName: true, testable: true },
TELEGRAM: { label: "Telegram", kind: "MESSENGER", secretLabel: "Токен бота", defaultBaseUrl: "https://api.telegram.org", hasModel: false, hasBotName: true, testable: true },
WEB: { label: "Web-виджет", kind: "MESSENGER", secretLabel: "", defaultBaseUrl: "", hasModel: false, hasBotName: false, testable: false },
OPENROUTER: { label: "OpenRouter", kind: "LLM_PROVIDER", secretLabel: "API-ключ", defaultBaseUrl: "https://openrouter.ai/api/v1", hasModel: true, testable: true },
MAX: { label: "MAX", kind: "MESSENGER", secretLabel: "Токен бота", defaultBaseUrl: "https://platform-api.max.ru", hasModel: false, testable: true },
TELEGRAM: { label: "Telegram", kind: "MESSENGER", secretLabel: "Токен бота", defaultBaseUrl: "https://api.telegram.org", hasModel: false, testable: true },
WEB: { label: "Web-виджет", kind: "MESSENGER", secretLabel: "", defaultBaseUrl: "", hasModel: false, testable: false },
};
export const STATUS_META: Record<IntegrationStatus, { label: string; bg: string; color: string }> = {