Прокси на внутренний IP: анализ и скачивания через REST-эндпоинты WordPress (режим proxy/direct, настройки)

This commit is contained in:
apuc committed 2026-09-14 16:41:44 +03:00
1 parent b3ac9491f6
commit 867fe7bc66
3 files changed
+191 -15

No files matched your search

+27 -5
View File
@@ -37,23 +37,44 @@ zip -r med-diagnostics.zip . -x README.md
## Настройки
**Настройки → Med Diagnostics** — поле «URL API-бэкенда» (по умолчанию `https://med.my-dpr.ru`).
**Настройки → Med Diagnostics**:
| Поле | Описание |
|---|---|
| **Режим подключения** | `Через сервер (внутренний IP)` — браузер шлёт видео на этот WordPress, PHP проксирует запрос на внутренний IP бэкенда (по умолчанию, рекомендуется когда WordPress и API на одном сервере). `Напрямую из браузера (CORS)` — браузер обращается к публичному URL API. |
| **Внутренний URL бэкенда** | Адрес API, доступный с этого сервера (по умолчанию `http://127.0.0.1:18018`). Используется в режиме «Через сервер». |
| **Публичный URL API-бэкенда** | Публичный адрес Flask-приложения (по умолчанию `https://med.my-dpr.ru`). Используется в режиме «Напрямую» и для ссылки «Поделиться». |
Можно также переопределить кодом:
```php
add_filter('med_diag_api_url', fn() => 'https://другой-домен.ru');
add_filter('med_diag_internal_url', fn() => 'http://10.0.0.5:18018');
add_filter('med_diag_mode', fn() => 'direct');
```
## Как это работает
- Плагин выводит ту же вёрстку, что и главная страница сервиса (стили с префиксом `med-`, чтобы не конфликтовать с темой).
- JS отправляет видео на API-бэкенд (`POST /analyze`) с абсолютным URL — поэтому нужен CORS на стороне Flask.
- **Режим «Через сервер» (по умолчанию)**: JS отправляет видео на REST-эндпоинт этого же WordPress (`/wp-json/med-diag/v1/analyze`), PHP пересылает его на внутренний IP бэкенда (`http://127.0.0.1:18018/analyze`). Скачивания PDF/DOCX/видео идут через `/wp-json/med-diag/v1/download/<id>/<type>`. CORS не нужен — все запросы same-origin.
- **Режим «Напрямую»**: JS отправляет видео на публичный URL API (`POST /analyze`) — тогда нужен CORS на стороне Flask.
- Результат (сводка, показатели, рекомендации, кнопки PDF/DOCX/видео/поделиться) рендерится на странице WordPress.
- Сканирование с камеры работает на мобильных (canvas 720×720 + MediaRecorder).
## Требования к серверу WordPress (режим «Через сервер»)
Видео сначала загружается в PHP, поэтому в `php.ini` должны быть подняты лимиты:
```ini
upload_max_filesize = 100M
post_max_size = 100M
max_execution_time = 300
```
Проверить текущие значения: `php -i | grep -E "upload_max_filesize|post_max_size|max_execution_time"`.
## Требования к бэкенду (Flask)
В `app.py` серверного репозитория уже добавлена поддержка CORS:
В `app.py` серверного репозитория уже добавлена поддержка CORS (нужна только в режиме «Напрямую»):
- `Access-Control-Allow-Origin` — по умолчанию `*`, можно ограничить через env `CORS_ORIGINS` (список через запятую, например `https://site1.ru,https://site2.ru`).
- Preflight `OPTIONS /analyze` возвращает `204`.
@@ -69,7 +90,8 @@ add_filter('med_diag_api_url', fn() => 'https://другой-домен.ru');
- Шорткод используйте **один раз на странице** (id элементов фиксированы).
- Для работы сканирования с камеры нужен HTTPS на сайте WordPress (getUserMedia требует secure context).
- Загрузка видео идёт напрямую в API-бэкенд — убедитесь, что `client_max_body_size` в nginx бэкенда ≥ 100 МБ.
- В режиме «Через сервер» видео загружается в PHP — следите за лимитами `upload_max_filesize`/`post_max_size`/`max_execution_time` (см. выше).
- Ссылка «Поделиться» всегда ведёт на публичный URL API (`/r/<share_id>`).
## Структура
+19 -5
View File
@@ -3,6 +3,9 @@
'use strict';
var API_URL = (window.MED_DIAG && MED_DIAG.apiUrl) || 'https://med.my-dpr.ru';
var PROXY_URL = (window.MED_DIAG && MED_DIAG.proxyUrl) || '';
var MODE = (window.MED_DIAG && MED_DIAG.mode) || 'proxy';
var USE_PROXY = (MODE === 'proxy') && !!PROXY_URL;
// Абсолютный URL: если ссылка относительная — добавляем домен API
function absUrl(url) {
@@ -10,6 +13,13 @@
return url.indexOf('http') === 0 ? url : API_URL + url;
}
// URL для скачивания: в режиме прокси — через этот же WordPress (PHP → внутренний IP)
function downloadUrl(path) {
if (!path) return path;
if (path.indexOf('http') === 0) return path;
return USE_PROXY ? PROXY_URL + path : API_URL + path;
}
var uploadArea = document.getElementById('uploadArea');
var fileInput = document.getElementById('fileInput');
var analyzeBtn = document.getElementById('analyzeBtn');
@@ -61,11 +71,15 @@
var formData = new FormData();
formData.append('video', blob, filename);
// В режиме прокси шлём на этот же WordPress (PHP перешлёт на внутренний IP бэкенда),
// иначе — напрямую на публичный URL API.
var analyzeUrl = USE_PROXY ? PROXY_URL + '/analyze' : API_URL + '/analyze';
// Автоповтор при обрыве соединения (нестабильная мобильная сеть)
var MAX_ATTEMPTS = 3;
for (var attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
var response = await fetch(API_URL + '/analyze', {
var response = await fetch(analyzeUrl, {
method: 'POST',
body: formData
});
@@ -119,7 +133,7 @@
var html = '<div class="med-error-box"><strong>Ошибка:</strong> ' + msg + '</div>';
if (videoUrl) {
html += '<div style="text-align:center; margin-top:15px">' +
'<a class="med-btn-download" href="' + absUrl(videoUrl) + '">🎬 Скачать записанное видео</a>' +
'<a class="med-btn-download" href="' + downloadUrl(videoUrl) + '">🎬 Скачать записанное видео</a>' +
'</div>';
}
resultContent.innerHTML = html;
@@ -134,10 +148,10 @@
// Кнопки скачивания (вверху блока результатов)
if (data.share_id) {
html += '<div class="med-download-buttons">' +
'<a class="med-btn-download" href="' + API_URL + '/download/' + data.share_id + '/pdf">📄 Скачать PDF</a>' +
'<a class="med-btn-download" href="' + API_URL + '/download/' + data.share_id + '/docx">📝 Скачать DOCX</a>';
'<a class="med-btn-download" href="' + downloadUrl('/download/' + data.share_id + '/pdf') + '">📄 Скачать PDF</a>' +
'<a class="med-btn-download" href="' + downloadUrl('/download/' + data.share_id + '/docx') + '">📝 Скачать DOCX</a>';
if (data.video_url) {
html += '<a class="med-btn-download" href="' + absUrl(data.video_url) + '">🎬 Скачать видео</a>';
html += '<a class="med-btn-download" href="' + downloadUrl(data.video_url) + '">🎬 Скачать видео</a>';
}
html += '<a class="med-btn-download" href="https://max.ru/miac_dnr_bot" target="_blank" rel="noopener">🏥 Запись на диспанцеризацию</a>' +
'</div>';
+145 -5
View File
@@ -2,7 +2,7 @@
/**
* Plugin Name: Med Diagnostics
* Description: Интеграция сервиса «Диагностика по видео» (med.my-dpr.ru) в WordPress. Выводит вёрстку сервиса через шорткод [med_diagnostics].
* Version: 1.0.0
* Version: 1.1.0
* Author: apuc
* License: GPL-2.0-or-later
* Text Domain: med-diagnostics
@@ -18,11 +18,12 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; // Прямой доступ запрещён
}
define( 'MED_DIAG_VERSION', '1.0.0' );
define( 'MED_DIAG_VERSION', '1.1.0' );
define( 'MED_DIAG_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
/**
* URL API-бэкенда (Flask-приложение med.my-dpr.ru).
* Используется в режиме «напрямую» и для публичных ссылок (share).
* Можно переопределить фильтром: add_filter('med_diag_api_url', fn() => 'https://...');
*/
function med_diag_api_url() {
@@ -30,6 +31,26 @@ function med_diag_api_url() {
return apply_filters( 'med_diag_api_url', untrailingslashit( $url ) );
}
/**
* Внутренний URL API-бэкенда (для проксирования через PHP).
* По умолчанию — внутренний IP сервера, где стоит и WordPress, и API.
* Можно переопределить фильтром: add_filter('med_diag_internal_url', fn() => 'http://10.0.0.5:18018');
*/
function med_diag_internal_url() {
$url = get_option( 'med_diag_internal_url', 'http://127.0.0.1:18018' );
return apply_filters( 'med_diag_internal_url', untrailingslashit( $url ) );
}
/**
* Режим подключения к бэкенду:
* 'proxy' — браузер шлёт запрос на этот же WordPress, PHP проксирует на внутренний IP (по умолчанию);
* 'direct' — браузер шлёт запрос напрямую на публичный URL API (нужен CORS на бэкенде).
*/
function med_diag_mode() {
$mode = get_option( 'med_diag_mode', 'proxy' );
return apply_filters( 'med_diag_mode', in_array( $mode, array( 'proxy', 'direct' ), true ) ? $mode : 'proxy' );
}
/* ─── Регистрация ассетов ─────────────────────────────────── */
add_action( 'wp_enqueue_scripts', 'med_diag_register_assets' );
@@ -49,10 +70,101 @@ function med_diag_register_assets() {
true // в футере
);
wp_localize_script( 'med-diag-js', 'MED_DIAG', array(
'apiUrl' => med_diag_api_url(),
'apiUrl' => med_diag_api_url(),
'proxyUrl' => rest_url( 'med-diag/v1' ),
'mode' => med_diag_mode(),
) );
}
/* ─── Прокси на внутренний IP (REST) ──────────────────────── */
add_action( 'rest_api_init', 'med_diag_register_routes' );
function med_diag_register_routes() {
// Анализ: браузер → этот WordPress → внутренний IP бэкенда
register_rest_route( 'med-diag/v1', '/analyze', array(
'methods' => 'POST',
'permission_callback' => '__return_true',
'callback' => 'med_diag_proxy_analyze',
) );
// Скачивания (PDF/DOCX/видео): /download/<id>/<pdf|docx|video>
register_rest_route( 'med-diag/v1', '/download/(?P<id>[^/]+)/(?P<type>[a-z]+)', array(
'methods' => 'GET',
'permission_callback' => '__return_true',
'callback' => 'med_diag_proxy_download',
) );
}
function med_diag_proxy_analyze( $request ) {
$files = $request->get_file_params();
if ( empty( $files['video'] ) ) {
return new WP_Error( 'med_diag_no_video', 'Файл video не получен', array( 'status' => 400 ) );
}
$file = $files['video'];
if ( $file['error'] !== UPLOAD_ERR_OK ) {
return new WP_Error( 'med_diag_upload_error', 'Ошибка загрузки файла (код ' . $file['error'] . ')', array( 'status' => 400 ) );
}
if ( $file['size'] > 100 * 1024 * 1024 ) {
return new WP_Error( 'med_diag_too_large', 'Видео больше 100 МБ', array( 'status' => 413 ) );
}
$boundary = wp_generate_password( 24, false );
$body = '--' . $boundary . "\r\n";
$body .= 'Content-Disposition: form-data; name="video"; filename="' . basename( $file['name'] ) . '"' . "\r\n";
$body .= 'Content-Type: ' . $file['type'] . "\r\n\r\n";
$body .= file_get_contents( $file['tmp_name'] ) . "\r\n";
$body .= '--' . $boundary . "--\r\n";
$response = wp_remote_post( med_diag_internal_url() . '/analyze', array(
'timeout' => 300,
'headers' => array( 'Content-Type' => 'multipart/form-data; boundary=' . $boundary ),
'body' => $body,
) );
if ( is_wp_error( $response ) ) {
return new WP_Error( 'med_diag_proxy_error', 'Бэкенд недоступен: ' . $response->get_error_message(), array( 'status' => 502 ) );
}
$status = wp_remote_retrieve_response_code( $response );
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return new WP_Error( 'med_diag_bad_response', 'Некорректный ответ бэкенда', array( 'status' => 502 ) );
}
return new WP_REST_Response( $data, $status );
}
function med_diag_proxy_download( $request ) {
$id = rawurlencode( $request['id'] );
$type = $request['type'];
$response = wp_remote_get( med_diag_internal_url() . '/download/' . $id . '/' . $type, array(
'timeout' => 300,
) );
if ( is_wp_error( $response ) ) {
return new WP_Error( 'med_diag_proxy_error', 'Бэкенд недоступен', array( 'status' => 502 ) );
}
$status = wp_remote_retrieve_response_code( $response );
if ( $status !== 200 ) {
return new WP_Error( 'med_diag_download_error', 'Ошибка скачивания (HTTP ' . $status . ')', array( 'status' => $status ) );
}
$body = wp_remote_retrieve_body( $response );
$headers = wp_remote_retrieve_headers( $response );
$content_type = isset( $headers['content-type'] ) ? $headers['content-type'] : 'application/octet-stream';
$filename = 'diagnostic_' . $request['id'] . '.' . ( 'video' === $type ? 'webm' : $type );
header( 'Content-Type: ' . $content_type );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Content-Length: ' . strlen( $body ) );
echo $body;
exit;
}
/* ─── Шорткод ─────────────────────────────────────────────── */
add_shortcode( 'med_diagnostics', 'med_diag_shortcode' );
@@ -150,6 +262,16 @@ function med_diag_register_settings() {
'sanitize_callback' => 'esc_url_raw',
'default' => 'https://med.my-dpr.ru',
) );
register_setting( 'med_diag_settings', 'med_diag_internal_url', array(
'type' => 'string',
'sanitize_callback' => 'esc_url_raw',
'default' => 'http://127.0.0.1:18018',
) );
register_setting( 'med_diag_settings', 'med_diag_mode', array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => 'proxy',
) );
}
function med_diag_settings_page() {
@@ -160,11 +282,29 @@ function med_diag_settings_page() {
<?php settings_fields( 'med_diag_settings' ); ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="med_diag_api_url">URL API-бэкенда</label></th>
<th scope="row"><label for="med_diag_mode">Режим подключения</label></th>
<td>
<select id="med_diag_mode" name="med_diag_mode">
<option value="proxy" <?php selected( med_diag_mode(), 'proxy' ); ?>>Через сервер (внутренний IP)</option>
<option value="direct" <?php selected( med_diag_mode(), 'direct' ); ?>>Напрямую из браузера (CORS)</option>
</select>
<p class="description">«Через сервер» — браузер шлёт видео на этот WordPress, PHP проксирует запрос на внутренний IP бэкенда (рекомендуется, когда WordPress и API на одном сервере). «Напрямую» — браузер обращается к публичному URL API (нужен CORS на бэкенде).</p>
</td>
</tr>
<tr>
<th scope="row"><label for="med_diag_internal_url">Внутренний URL бэкенда</label></th>
<td>
<input type="url" id="med_diag_internal_url" name="med_diag_internal_url"
value="<?php echo esc_attr( med_diag_internal_url() ); ?>" class="regular-text" />
<p class="description">Адрес API-бэкенда, доступный с этого сервера (по умолчанию http://127.0.0.1:18018). Используется в режиме «Через сервер».</p>
</td>
</tr>
<tr>
<th scope="row"><label for="med_diag_api_url">Публичный URL API-бэкенда</label></th>
<td>
<input type="url" id="med_diag_api_url" name="med_diag_api_url"
value="<?php echo esc_attr( med_diag_api_url() ); ?>" class="regular-text" />
<p class="description">Адрес Flask-приложения, которое выполняет анализ (по умолчанию https://med.my-dpr.ru).</p>
<p class="description">Публичный адрес Flask-приложения (по умолчанию https://med.my-dpr.ru). Используется в режиме «Напрямую» и для ссылки «Поделиться».</p>
</td>
</tr>
</table>