В этой статье будет показано, как разработать навык для Яндекс Алисы, позволяющий удалённо управлять компьютером. Для реализации мы будем использовать языки Kotlin и Java.
Мне такой навык понадобился для управления медиаплеером — например, чтобы ставить видео на паузу, регулировать громкость, переключать треки или видео, перематывать назад или вперёд, открывать определённые фильмы на Кинопоиске. Я часто использую компьютер как телевизор, и возможность голосового управления делает использование гораздо удобнее.
Архитектура решения
Определим архитектуру приложения. Она будет состоять из клиентских и серверных нод.
-
При запуске клиентская нода отправляет запрос на серверную ноду, передавая следующие данные: имя ноды, хост, порт.
-
При отключении клиентская нода также отправляет запрос с указанием своего имени для удаления из кластера.
-
Клиентских нод может быть несколько — они идентифицируются по имени.
Серверная нода выполняет следующие функции:
-
Хранит информацию о клиентских нодах (имя, адрес, порт).
-
Принимает команды от пользователя (например, через навык Алисы).
-
Определяет, какую команду нужно выполнить и на какой клиентской ноде, используя шаблоны, описанные в конфигурации.
-
Пересылает команду соответствующей клиентской ноде для выполнения.

Таким образом, сервер выступает как центральный координатор между внешними интерфейсами и реальными исполнительными агентами (клиентами).
Клиентская нода
Начнём с разработки клиентской ноды. Основные задачи:
-
Отправить запрос на серверную ноду для подключения к кластеру.
-
Принимать HTTP-запросы и выполнять команды.
-
При завершении работы отправить запрос на серверную ноду для отключения.
Информацию о командах, которые может выполнять клиентская нода, мы будем хранить в конфигурационном файле 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
. -
В зависимости от результата выполнения команды навык возвращает один из двух вариантов ответа: «Команда успешно выполнена» или «Не удалось выполнить команду».
Полезные ссылки
ссылка на оригинал статьи https://habr.com/ru/articles/900450/
Добавить комментарий