Initial commit: omnichannel-mcp — MCP-сервер управления платформой Omnichannel
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
package configserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// api_observe.go — read-only операции (наблюдение и диагностика).
|
||||
|
||||
// WhoAmI — ответ GET /api/whoami.
|
||||
type WhoAmI struct {
|
||||
Authenticated bool `json:"authenticated"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// Capabilities — результат capability-probe: версия API и доступные группы
|
||||
// JSON-эндпоинтов v1.1.0. Нужен, чтобы инструменты не «тихо» ломались на
|
||||
// старых стендах, а возвращали внятное сообщение.
|
||||
type Capabilities struct {
|
||||
Version string `json:"version"`
|
||||
Features map[string]bool `json:"features"`
|
||||
}
|
||||
|
||||
// Health проверяет живость сервера (GET /health, без авторизации).
|
||||
func (a *API) Health(ctx context.Context) (int, string, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
resp, err := a.Sess.getPublic(ctx, "/health")
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
return resp.status, strings.TrimSpace(string(resp.body)), nil
|
||||
}
|
||||
|
||||
// WhoAmI проверяет текущую авторизацию.
|
||||
func (a *API) WhoAmI(ctx context.Context) (*WhoAmI, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
out := &WhoAmI{}
|
||||
if err := a.Sess.getJSON(ctx, "/api/whoami", nil, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Probe определяет версию API. Ключевой признак v1.1.0 — публичный JSON
|
||||
// `/api/whoami` (в <=1.0.20 его нет, middleware отвечает 401).
|
||||
func (a *API) Probe(ctx context.Context) (*Capabilities, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
resp, err := a.Sess.getPublic(ctx, "/api/whoami")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caps := &Capabilities{Version: "legacy (<1.1.0)", Features: map[string]bool{}}
|
||||
if resp.status == 200 && strings.Contains(resp.ctype, "json") {
|
||||
caps.Version = "1.1.0"
|
||||
for _, f := range []string{
|
||||
"api_login", "api_whoami", "application_detail",
|
||||
"set_compose", "set_env", "tasks_filter", "task_get", "update_front",
|
||||
} {
|
||||
caps.Features[f] = true
|
||||
}
|
||||
}
|
||||
return caps, nil
|
||||
}
|
||||
|
||||
// ListApplications возвращает список сервисов.
|
||||
func (a *API) ListApplications(ctx context.Context, includePresence bool) ([]Application, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
q := url.Values{}
|
||||
if includePresence {
|
||||
q.Set("include_presence", "true")
|
||||
}
|
||||
var out []Application
|
||||
if err := a.Sess.getJSON(ctx, "/api/applications", q, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetApplication возвращает детализацию сервиса.
|
||||
func (a *API) GetApplication(ctx context.Context, id int) (*ApplicationDetail, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
out := &ApplicationDetail{}
|
||||
if err := a.Sess.getJSON(ctx, "/api/application/"+strconv.Itoa(id), nil, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetConfig возвращает текущие compose/env сервиса (эндпоинт агента, публичный).
|
||||
func (a *API) GetConfig(ctx context.Context, id int) (*RawConfig, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
out := &RawConfig{}
|
||||
if err := a.Sess.getJSON(ctx, "/api/get_config/"+strconv.Itoa(id), nil, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// EnvFiles возвращает имена текущих env-файлов сервиса.
|
||||
func (a *API) EnvFiles(ctx context.Context, id int) ([]string, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
var out []string
|
||||
if err := a.Sess.getJSON(ctx, "/api/env_files/"+strconv.Itoa(id), nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ComposeVersion возвращает конкретную версию compose.
|
||||
func (a *API) ComposeVersion(ctx context.Context, versionID int) (*ComposeVersion, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
out := &ComposeVersion{}
|
||||
if err := a.Sess.getJSON(ctx, "/api/compose_version/"+strconv.Itoa(versionID), nil, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// EnvVersion возвращает конкретную версию env-файла.
|
||||
func (a *API) EnvVersion(ctx context.Context, versionID int) (*EnvVersion, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
out := &EnvVersion{}
|
||||
if err := a.Sess.getJSON(ctx, "/api/env_version/"+strconv.Itoa(versionID), nil, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// EnvVersions возвращает все версии конкретного env-файла.
|
||||
func (a *API) EnvVersions(ctx context.Context, appID int, filename string) ([]EnvVersion, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
path := "/api/env_versions/" + strconv.Itoa(appID) + "/" + url.PathEscape(filename)
|
||||
var out []EnvVersion
|
||||
if err := a.Sess.getJSON(ctx, path, nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// TaskFilter — фильтры списка задач.
|
||||
type TaskFilter struct {
|
||||
HostIP string
|
||||
Status string
|
||||
TaskType string
|
||||
Limit int
|
||||
}
|
||||
|
||||
// ListTasks возвращает задачи с фильтрами (GET /api/tasks).
|
||||
func (a *API) ListTasks(ctx context.Context, f TaskFilter) ([]Task, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
q := url.Values{}
|
||||
if f.HostIP != "" {
|
||||
q.Set("host_ip", f.HostIP)
|
||||
}
|
||||
if f.Status != "" {
|
||||
q.Set("status", f.Status)
|
||||
}
|
||||
if f.TaskType != "" {
|
||||
q.Set("task_type", f.TaskType)
|
||||
}
|
||||
if f.Limit > 0 {
|
||||
q.Set("limit", strconv.Itoa(f.Limit))
|
||||
}
|
||||
var out []Task
|
||||
if err := a.Sess.getJSON(ctx, "/api/tasks", q, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// GetTask возвращает задачу по task_id.
|
||||
func (a *API) GetTask(ctx context.Context, taskID string) (*Task, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
out := &Task{}
|
||||
if err := a.Sess.getJSON(ctx, "/api/task/"+url.PathEscape(taskID), nil, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// WaitTask опрашивает задачу до завершения (completed/failed) или до истечения
|
||||
// task_poll_max_sec. Возвращает последнее известное состояние задачи; признак
|
||||
// незавершённости — Status == "pending"/"in_progress". Loop агента
|
||||
// последовательный, поэтому ожидание строго ограничено конфигом.
|
||||
func (a *API) WaitTask(ctx context.Context, taskID string) (*Task, error) {
|
||||
deadline := time.Now().Add(time.Duration(a.Srv.TaskPollMaxSec) * time.Second)
|
||||
interval := time.Duration(a.Srv.TaskPollIntervalSec) * time.Second
|
||||
if interval <= 0 {
|
||||
interval = time.Second
|
||||
}
|
||||
for {
|
||||
task, err := a.GetTask(ctx, taskID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if task.Status == "completed" || task.Status == "failed" {
|
||||
return task, nil
|
||||
}
|
||||
if !time.Now().Before(deadline) {
|
||||
return task, nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return task, nil
|
||||
case <-time.After(interval):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeploymentFiles возвращает schema/manifest/prebuild_vars/ip_overrides.
|
||||
func (a *API) DeploymentFiles(ctx context.Context) (*DeploymentFiles, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
out := &DeploymentFiles{}
|
||||
if err := a.Sess.getJSON(ctx, "/api/deployment/files", nil, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// IPMatch возвращает диагностику сопоставления IP схемы и агентов.
|
||||
func (a *API) IPMatch(ctx context.Context) (*IPMatch, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
out := &IPMatch{}
|
||||
if err := a.Sess.getJSON(ctx, "/api/deployment/ip_match", nil, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ReleaseJob возвращает статус фоновой загрузки релиза.
|
||||
func (a *API) ReleaseJob(ctx context.Context) (*ReleaseJob, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
out := &ReleaseJob{}
|
||||
if err := a.Sess.getJSON(ctx, "/api/deployment/release_job", nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DeploymentTasks возвращает последние release-задачи (sync/start).
|
||||
func (a *API) DeploymentTasks(ctx context.Context) ([]Task, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
var out []Task
|
||||
if err := a.Sess.getJSON(ctx, "/api/deployment/tasks", nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Stats возвращает счётчики версий.
|
||||
func (a *API) Stats(ctx context.Context) (*Stats, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
out := &Stats{}
|
||||
if err := a.Sess.getJSON(ctx, "/api/stats", nil, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ServiceMap возвращает карту серверов и сервисов.
|
||||
func (a *API) ServiceMap(ctx context.Context) (*ServiceMap, error) {
|
||||
ctx, cancel := a.withTimeout(ctx)
|
||||
defer cancel()
|
||||
out := &ServiceMap{}
|
||||
if err := a.Sess.getJSON(ctx, "/api/service_map", nil, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user