Мультиклиент для AI-агентов на Go и Flutter: Planulix 2026 | AiManual
AiManual Logo Ai / Manual.
12 Май 2026 Гайд

Как создать мультиклиентский центр управления для AI-агентов: Planulix на Go и Flutter

Соберите собственный центр управления для Claude Code, Cursor, Codex и Kimi. Go-шлюз, Flutter UI, VPS — решение проблемы блокировок аккаунтов. Полный гайд с код

Почему ваш AI-агент однажды просто перестанет отвечать

Вы сидите за терминалом, запускаете claude или cursor, а в ответ — тишина. Или, что еще хуже, — 429 Too Many Requests. Знакомо? Если вы активно используете AI-агентов для разработки (Claude Code, Cursor, Codex, Kimi), то рано или поздно упираетесь в блокировку аккаунта по IP или API-лимиту. Провайдеры видят, что с одного адреса идет подозрительно много запросов, и банально отрубают доступ.

В 2026 году эта проблема стала еще острее. Anthropic, OpenAI, Cursor, Moonshot (Kimi) ужесточили антифрод-политики. Обычные прокси и VPN не спасают — их IP-пулы тоже в черных списках. Нужна архитектура, которая:

  • Разделяет трафик между несколькими аккаунтами.
  • Позволяет переключать провайдера на лету.
  • Работает на вашем VPS, а не через публичные сервера.
  • Дает единый UI для управления агентами.

Я прошел через это. Собрал Planulix — мультиклиентский центр управления AI-агентами на Go (backend) и Flutter (frontend). Сегодня разберу архитектуру магистраль и покажу, как вы можете развернуть такой же за вечер. Кстати, если вы еще не знакомы с концепцией AI-агентов в UI, советую глянуть статью CopilotKit и AG-UI — там похожий протокол, но для веба.

Анатомия Planulix: три слоя, которые держат удар

Planulix состоит из трех компонентов:

  1. Go Gateway — высокопроизводительный шлюз, который маршрутизирует запросы к API разных провайдеров, балансирует нагрузку и ротирует ключи.
  2. Flutter Client — десктопное и мобильное приложение для управления агентами, просмотра логов и переключения профилей.
  3. CLI-обертка — агностичный интерфейс для запуска локальных агентов (Claude Code, Cursor CLI) через шлюз.

Все это живет на вашем VPS. Никаких зависимостей от облачных сервисов — только ваш сервер и ваши ключи. Вдохновлялся архитектурой из Plano 0.4.3, но адаптировал под мультиклиент.

Суть: вместо того чтобы каждый AI-агент сам ходил в API, все запросы идут через Planulix Gateway. Шлюз решает, какой провайдер ответит, и возвращает результат. Клиент (Flutter или CLI) при этом может переключать профили без перезапуска.

Строим Go Gateway: сердце Planulix

Go идеально подходит для такой задачи: легковесные горутины, быстрый JSON, встроенный HTTP/2, простое развертывание в один бинарник. Я использовал Go 1.23 (на момент 2026 года уже Go 1.25, но код обратно совместим).

1 Архитектура конфигурации

Конфигурация — это YAML-файл, где описаны провайдеры, их API-эндпоинты и ключи. Почему YAML, а не JSON? Потому что YAML с комментариями удобнее читать и править на сервере.

# /etc/planulix/gateway.yaml
providers:
  claude:
    type: anthropic
    base_url: "https://api.anthropic.com/v1"
    keys:
      - "sk-ant-...1"
      - "sk-ant-...2"
    rate_limit: 10  # запросов в секунду
  cursor:
    type: cursor
    base_url: "https://api.cursor.sh"
    keys:
      - "cur-...a"
    rate_limit: 5
  codex:
    type: openai
    base_url: "https://api.openai.com/v1"
    keys:
      - "sk-...x1"
      - "sk-...x2"
    rate_limit: 20
  kimi:
    type: moonshot
    base_url: "https://api.moonshot.cn/v1"
    keys:
      - "mk-...k1"
    rate_limit: 3

Шлюз читает конфиг при старте и может перезагружаться по сигналу SIGHUP. Никакого даунтайма.

2 Маршрутизация запросов

Каждый входящий запрос должен содержать заголовок X-Planulix-Provider (или дефолтный из профиля). Шлюз проверяет лимиты, выбирает ключ по round-robin или least-used и отправляет запрос к нужному провайдеру. Ответ проксируется обратно.

Вот фрагмент ядра на Go — не копируйте слепо, а вникайте в логику:

package gateway

import (
	"context"
	"net/http"
	"sync"
	"time"
)

type Provider struct {
	Name     string
	BaseURL  string
	Keys     []string
	KeyIndex int
	mu       sync.Mutex
	Limiter  *RateLimiter
}

func (p *Provider) NextKey() string {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.KeyIndex = (p.KeyIndex + 1) % len(p.Keys)
	return p.Keys[p.KeyIndex]
}

type RateLimiter struct {
	tokens  chan struct{}
	closeCh chan struct{}
}

func NewRateLimiter(rps int) *RateLimiter {
	rl := &RateLimiter{
		tokens:  make(chan struct{}, rps),
		closeCh: make(chan struct{}),
	}
	go func() {
		ticker := time.NewTicker(time.Second / time.Duration(rps))
		defer ticker.Stop()
		for {
			select {
			case <-ticker.C:
				select {
				case rl.tokens <- struct{}{}:
				default:
				}
			case <-rl.closeCh:
				return
			}
		}
	}()
	return rl
}

func (rl *RateLimiter) Wait(ctx context.Context) error {
	select {
	case <-rl.tokens:
		return nil
	case <-ctx.Done():
		return ctx.Err()
	}
}

// ProxyHandler - основной обработчик
func ProxyHandler(w http.ResponseWriter, r *http.Request) {
	providerName := r.Header.Get("X-Planulix-Provider")
	if providerName == "" {
		providerName = "claude" // default
	}
	provider := getProvider(providerName)
	if provider == nil {
		http.Error(w, "provider not found", http.StatusBadRequest)
		return
	}
	
	if err := provider.Limiter.Wait(r.Context()); err != nil {
		http.Error(w, "rate limit", http.StatusTooManyRequests)
		return
	}
	
	key := provider.NextKey()
	// ... клонируем запрос, подменяем Authorization, проксируем
}

Здесь два важных момента. Первый — RateLimiter работает на токенах с фиксированной скоростью, а не на окнах. Это дает равномерную нагрузку, что снижает риск блокировки. Второй — выбор ключа round-robin. Каждый ключ используется циклически, так что ни один аккаунт не перегревается.

Типичная ошибка: не добавлять контекстную таймауты. Если провайдер завис, ваша горутина будет ждать вечно, а клиенты получат 502. Всегда ставьте таймаут не более 30 секунд.

3 Health-чеки и автоотключение

Провайдеры иногда возвращают ошибки аутентификации (401) или превышение квоты (403). Шлюз должен это отслеживать и временно выводить ключ из ротации. Добавил счетчик ошибок: если за последние 5 минут больше 10 неудач — ключ в blacklist на 30 минут.

func (p *Provider) RecordFailure(keyIdx int) {
	p.mu.Lock()
	defer p.mu.Unlock()
	// увеличиваем счетчик ошибок для конкретного ключа
}

func (p *Provider) IsKeyHealthy(keyIdx int) bool {
	// проверяем, не превышен ли порог
}

Flutter Client: единое окно во все агенты

Зачем Flutter, если можно сделать веб-интерфейс? Потому что Flutter дает нативный опыт на десктопе (Windows, macOS, Linux) и мобилках. Вы можете мониторить агенты с телефона, а CLI-обертка работает на сервере. Planulix Client подключается к Go Gateway через gRPC или REST (я выбрал REST для простоты).

Архитектура Flutter-приложения

  • Слой моделей: ProviderConfig, Session, LogEntry.
  • Слой репозитория: абстракция над HTTP-клиентом (Dio).
  • BLoC для управления состоянием: переключение провайдера, просмотр логов, редактирование профилей.
  • UI: Material 3 с кастомными темами (адаптация под светлую тему, как у нас на сайте).

Ключевая особенность — пул соединений. Gateway поддерживает долгие WebSocket-соединения для стриминга ответов от AI-агентов. Flutter открывает WebSocket и получает чанки ответа в реальном времени, отображая их в терминальном окне.

4 Пример виджета для переключения провайдера

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

class ProviderSwitcher extends StatelessWidget {
  const ProviderSwitcher({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<GatewayCubit, GatewayState>(
      builder: (context, state) {
        return DropdownButton<String>(
          value: state.currentProvider,
          items: state.availableProviders.map((p) {
            return DropdownMenuItem(value: p, child: Text(p));
          }).toList(),
          onChanged: (provider) {
            if (provider != null) {
              context.read<GatewayCubit>().switchProvider(provider);
            }
          },
        );
      },
    );
  }
}
💡
Совет: для мобильной версии используйте BottomSheet или CupertinoAlertDialog. На десктопе — DropdownButton с иконками провайдеров.

CLI-обертка и интеграция с локальными агентами

Чтобы ваш локальный Claude Code или Cursor CLI общались через Planulix, нужно переопределить их API-эндпоинт. У Claude Code есть флаг --api-base или переменная окружения ANTHROPIC_BASE_URL. Мы создаем простой shell-wraper:

#!/bin/bash
# planulix-wraper.sh
# Запускает Claude Code через Planulix Gateway

export ANTHROPIC_BASE_URL="http://localhost:9090/anthropic"
export ANTHROPIC_API_KEY="dummy-key"  # шлюз игнорирует, берет свой

# Опционально: указать провайдер через env
export PLANULIX_PROVIDER="claude"

claude "$@"

Для Cursor это сложнее — он использует WebSocket и свой протокол. Пришлось написать небольшой reverse-proxy в Go, который перехватывает handshake и добавляет заголовок X-Planulix-Provider. Детали выходят за рамки статьи, но код выложу в репозиторий.

Развертывание на VPS: от греха подальше

Я арендовал дешевый VPS на Hetzner (2 ядра, 4 ГБ RAM) с Ubuntu 24.04. Planulix Gateway и Flutter-бэкенд (веб-версия) упаковал в Docker. Но можно и без контейнеров — бинарник на Go не требует зависимостей.

5 Docker Compose

version: '3.8'
services:
  gateway:
    build: ./gateway
    ports:
      - "9090:9090"
    volumes:
      - ./config:/etc/planulix
    environment:
      - PLANULIX_CONFIG_PATH=/etc/planulix/gateway.yaml
    restart: unless-stopped
  web:
    build: ./flutter_client/build/web
    ports:
      - "8080:80"
    depends_on:
      - gateway

После старта проверьте, что шлюз отвечает:

curl -H "X-Planulix-Provider: claude" http://your-vps:9090/v1/health

Если ответ {"status":"ok"} — все работает.

Важно: не забудьте настроить ufw, чтобы порты были открыты только для ваших IP. Иначе через день шлюз завалят боты. Я использую fail2ban с кастомной джиттер-задержкой.

Как НЕ надо: частые ошибки при сборке мультиклиента

  • Отсутствие retry с экспоненциальной задержкой. Если провайдер вернул 429, вы должны подождать и повторить с другим ключом. Без этого клиент будет падать.
  • Жесткая привязка к одному провайдеру. Я изначально захардкодил Claude Code. Когда Anthropic заблокировал мой ключ, пришлось экстренно добавлять Codex. Сделайте провайдеры плагинными с самого начала.
  • Синхронная блокировка UI во Flutter. Никогда не делайте HTTP-вызовы в основном потоке. Используйте BLoC или Riverpod для асинхронной работы.
  • Хранение ключей в репозитории. Ключи должны быть в переменных окружения или Vault, а не в коде. Хотя бы .gitignore.

Что дальше? План развития Planulix

Сегодня Planulix умеет маршрутизировать запросы, балансировать по ключам, показывать логи и статистику в Flutter UI. В планах на Q3 2026:

  • Поддержка OpenRouter (как в DeepSeek TUI).
  • Кастомные промпты-префиксы для каждого провайдера.
  • Кэширование ответов на уровне шлюза для идентичных запросов.

Если тема AI-агентов на десктопе интересна, почитайте как превратить Ollama в персонального ассистента — это частично пересекается с мультиклиентской идеей.

Финальный совет: не верьте в «один ключ навсегда»

Мир AI-агентов меняется каждые полгода. Провайдеры банят аккаунты за превышение лимитов, за слишком частые запросы, за подозрительную активность. Единственный способ не остаться без инструмента — иметь запасные ключи и уметь мгновенно переключаться между ними. Planulix дает вам эту возможность.

Соберите свой центр управления сегодня. Начните с малого — поднимите шлюз на Go, добавьте пару ключей от Claude и Codex, настройте Flutter-клиент. Через неделю вы не сможете представить работу без него.

P.S. Весь код Planulix (пока в черновике) я выложу на GitHub к концу месяца. Если хотите получить уведомление — подпишитесь на блог.

Подписаться на канал