mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-08 10:24:59 +03:00
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:
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 }> = {
|
||||
|
||||
Reference in new issue
Block a user