Как стать автором
Обновить

Разработка навыка Яндекс Алисы для удалённого управления компьютером

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров2.8K

В этой статье будет показано, как разработать навык для Яндекс Алисы, позволяющий удалённо управлять компьютером. Для реализации мы будем использовать языки Kotlin и Java.

Мне такой навык понадобился для управления медиаплеером — например, чтобы ставить видео на паузу, регулировать громкость, переключать треки или видео, перематывать назад или вперёд, открывать определённые фильмы на Кинопоиске. Я часто использую компьютер как телевизор, и возможность голосового управления делает использование гораздо удобнее.

Архитектура решения

Определим архитектуру приложения. Она будет состоять из клиентских и серверных нод.

  • При запуске клиентская нода отправляет запрос на серверную ноду, передавая следующие данные: имя ноды, хост, порт.

  • При отключении клиентская нода также отправляет запрос с указанием своего имени для удаления из кластера.

  • Клиентских нод может быть несколько — они идентифицируются по имени.

Серверная нода выполняет следующие функции:

  • Хранит информацию о клиентских нодах (имя, адрес, порт).

  • Принимает команды от пользователя (например, через навык Алисы).

  • Определяет, какую команду нужно выполнить и на какой клиентской ноде, используя шаблоны, описанные в конфигурации.

  • Пересылает команду соответствующей клиентской ноде для выполнения.

Таким образом, сервер выступает как центральный координатор между внешними интерфейсами и реальными исполнительными агентами (клиентами).

Клиентская нода

Начнём с разработки клиентской ноды. Основные задачи:

  1. Отправить запрос на серверную ноду для подключения к кластеру.

  2. Принимать HTTP-запросы и выполнять команды.

  3. При завершении работы отправить запрос на серверную ноду для отключения.

Информацию о командах, которые может выполнять клиентская нода, мы будем хранить в конфигурационном файле client_commands.toml.

[windows.post]
shutdown = "shutdown /s /t 1"
reboot = "shutdown /r /t 1"
sleep = "shutdown /h"

[macos.post]
shutdown = "sudo shutdown -h now"
reboot = "sudo shutdown -r now"
sleep = "pmset sleepnow"

[linux.post]
shutdown = "sudo shutdown -h now"
reboot = "sudo reboot"
sleep = "systemctl suspend"

Файл команд разделён на секции, каждая из которых указывает:

  • Первый префикс — операционная система, для которой предназначены команды (windows, macos, linux).

  • Второй префикс — HTTP-метод, по которому команда будет доступна (post, get и т.д.).

  • Далее — список команд, каждая из которых содержит:

    • название команды (например, shutdown, reboot, sleep);

    • строку с системной командой, которую нужно выполнить на ноде.

Каждая клиентская нода содержит файл конфигурации config.yml, в котором указаны ключевые параметры её работы:

name: "Компьютер"
host: "192.168.0.100"
port: 11301
server-base-url: "http://server-core/server-core/api/v1/"
  • name — уникальное имя клиентской ноды;

  • host — IP-адрес или доменное имя, по которому нода доступна в сети;

  • port — порт, на котором будет запущен HTTP-сервер;

  • server-base-url — URL серверной ноды, к которой подключается клиент.

При запуске приложения вызывается метод initializeApplication(), который выполняет несколько ключевых задач:

private static void initializeApplication() throws IOException {
  config = loadConfig(); // Загрузка конфигурации из config.yml
  serverCore = createServerCore(config.serverBaseUrl()); // Создание Retrofit-клиента
  registerShutdownHook(); // Регистрация shutdown hook для корректного отключения
}
  • Загрузка конфигурации: с помощью SnakeYAML читается config.yml, и значения помещаются в объект AppConfig.

  • Создание Retrofit-клиента: инициализируется ServerCore — интерфейс взаимодействия с серверной нодой.

  • Shutdown hook: при завершении работы приложения будет отправлен запрос unregisterNode на серверную ноду, чтобы удалить клиент из кластера.

После инициализации вызывается метод startApplication(), который регистрирует клиентскую ноду на сервере и, при успешной регистрации, запускает HTTP-сервер с командами:

private static void startApplication() {
  connectToServer(() -> {
    ParserCommand parser = new TomlParserCommand(); // Парсинг client_commands.toml
    var osCommands = parser.parse("/client_commands.toml", OSUtils.getOperatingSystem());

    app = createAndStartJavalinApp(config.port()); // Запуск HTTP-сервера Javalin
    registerPostCommands(app, osCommands.commandsTypePost()); // Регистрация команд POST
  });
}
  • connectToServer — отправляет запрос на регистрацию клиентской ноды на сервере. В случае успеха выполняется лямбда-функция.

  • client_commands.toml — содержит список системных команд, сгруппированных по ОС и HTTP-методу (как описано ранее).

  • Регистрация команд — каждая команда из TOML-файла становится доступной по HTTP-адресу: http://{host}:{port}/{имя_команды}

    Пример: команда reboot будет доступна по http://192.168.0.100:11301/reboot.

Каждая команда обрабатывается следующим образом

app.post(path, ctx -> executeCommand(ctx, command));

Внутри executeCommand создаётся процесс с помощью ProcessBuilder, и результат исполнения возвращается пользователю.

Полный исходный код клиентской ноды доступен на GitHub.

Серверная нода

Серверная нода (server-core) отвечает за хранение сведений о клиентских нодах и обработку поступающих команд. Команды поступают в виде текстового сообщения, содержащего имя ноды и требуемое действие (например, «перезагрузить node-1»).

Для распознавания и интерпретации таких команд используется конфигурационный файл, в котором задаются шаблоны сообщений и соответствующие команды, например:

remote-ops:
  commands:
    - messages:
        - "перезагрузить ${name}"
        - "перезагрузи ${name}"
      command: "reboot"
    - messages:
        - "выключить ${name}"
        - "выключи ${name}"
      command: "shutdown"

Основная логика находится в RemoteOpsService

Метод processRemoteOperation(message)отвечает за обработку поступившего сообщения. Он ищет подходящую команду, сравнивая шаблоны с текстом сообщения. В случае совпадения извлекается команда и выполняется на всех подходящих клиентских нодах:

public void processRemoteOperation(String message) {
    findMatchingCommand(message)
        .ifPresentOrElse(
            this::executeCommandOnAllNodes,
            () -> handleUnknownCommand(message)
        );
}

Поиск команды осуществляется по шаблону с подстановкой имени каждой зарегистрированной ноды:

private boolean matchesMessage(String template, String message) {
    return clientNodeService.getRegisteredNodeNames().stream()
        .anyMatch(nodeName ->
            message.equalsIgnoreCase(template.replace("${name}", nodeName))
        );
}

Если подходящая команда найдена, она отправляется всем нодам, имя которых соответствует шаблону.

Работа с клиентскими нодами — ClientNodeService

Класс ClientNodeService реализует весь функционал, связанный с клиентскими нодами:

  • Регистрация и удаление нод;

  • Хранение адресов;

  • Отправка команд;

Все зарегистрированные клиентские ноды хранятся в ConcurrentHashMap<String, String>, где ключ — это имя ноды, а значение — её сетевой адрес в формате host:port. Регистрация и удаление нод осуществляется через HTTP-интерфейс:

public void registerNode(String name, String host, int port) {
    String address = formatAddress(host, port);
    nodes.put(name, address);
    log.info("Node registered: {} -> {}", name, address);
}

public void unregisterNode(String name) {
    nodes.remove(name);
}

Вместо встроенного HashMap можно использовать внешнюю систему хранения, например Redis.

Выполнение команды на конкретной ноде:

public void executeCommandOnNode(String command, String nodeName) {
    validateNodeExists(nodeName);
    String url = buildCommandUrl(nodeName, command);
    Request request = buildPostRequest(url);

    try (Response response = httpClient.newCall(request).execute()) {
        handleResponse(response, nodeName, command);
    } catch (Exception e) {
        throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to execute command on node: " + nodeName, e);
    }
}

Каждая команда отправляется по шаблону http://<ip:port>/<command> с пустым телом POST-запроса.

HTTP-интерфейс

  • POST /client-nodes — регистрация новой ноды.

  • DELETE /client-nodes/{name} — удаление ноды.

  • POST /remote-operations — принимает JSON с полем message, обрабатывает и выполняет команду, если она определена.

Полный исходный код серверной ноды доступен на GitHub.

Навык Яндекс Алиса

Для взаимодействия пользователя с системой через голосовой интерфейс я реализовал навык Яндекс Алисы. Навык построен с использованием Kotlin-библиотеки alice-ktx, которая упрощает создание навыков. Вы также можете использовать любую другую клиентскую реализацию — будь то Web, AlexStar, Android или iOS приложение, — для отправки текстовых команд на сервер.

Пример реализации навыка на Kotlin:

fun main() {
    val remoteOpsService = RemoteOpsService(
        createRetrofitClient(SkillConfig.SERVER_BASE_URL)
    )

    skill {
        webhookServer = ktorWebhookServer {
            port = SkillConfig.WEBHOOK_PORT
            path = SkillConfig.WEBHOOK_PATH
        }

        dispatch {
            newSession {
                response {
                    text = "Привет! Я могу выполнять удаленные команды."
                }
            }

            message {
                val isSuccess = remoteOpsService.executeCommand(messageText)
                val responseText = if (isSuccess) {
                    "Команда успешно выполнена"
                } else {
                    "Не удалось выполнить команду"
                }

                response { text = responseText }
            }
        }
    }.run()
}

Что делает этот код:

  • При запуске навыка открывается HTTP‑сервер, принимающий вебхуки от Яндекс Алисы.

  • Когда пользователь впервые запускает сессию, навык приветствует его фразой: «Привет! Я могу выполнять удаленные команды.»

  • Когда пользователь произносит команду (например: «выключи сервер1»), навык отправляет текстовое сообщение на серверную ноду через RemoteOpsService.

  • В зависимости от результата выполнения команды навык возвращает один из двух вариантов ответа: «Команда успешно выполнена» или «Не удалось выполнить команду».

Полезные ссылки

Теги:
Хабы:
+1
Комментарии8

Публикации

Работа

Java разработчик
213 вакансий

Ближайшие события