feat(ai-doctor): supercharge AI Doctor with multi-fix, log context, history, and 13 quick fix codes
Backend (runtime.uc, agent_api.uc, tachyon CLI): - Enrich AI Doctor LLM context with Watchdog state (OOM, streaks, active repairs) and last 15 critical log lines - Support multi-fix parsing (quick_fixes array) and comma-separated FIX tags - Add persistent history saving to /tmp/ai_doctor_last.json and ai_doctor_last endpoint - Expand quick fix codes from 9 to 13 (added clear_dns_cache, update_subscriptions, reset_firewall, restart_network) - Implement apply_quick_fix CLI command and REST API endpoint Frontend (fe-app-tachyon, LuCI main.js): - Add AI Doctor action button in Diagnostics tab - Add shell methods aiDoctor, aiDoctorLast, applyQuickFix - Render AI Doctor modal with Multi-Fix buttons and 'Fix All' option - Integrate toast notifications and state updates
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Tachyon AI & Agent API Integration
|
||||
|
||||
Tachyon предоставляет встроенные инструменты интеграции с искусственным интеллектом:
|
||||
1. **AI Doctor** — автоматическая диагностика системы с участием LLM (OpenAI, Anthropic, DeepSeek или локальные/кастомные модели) и кнопками быстрого исправления (Quick Fix).
|
||||
1. **AI Doctor** — автоматическая диагностика системы с участием LLM (OpenAI, Anthropic, DeepSeek или локальные/кастомные модели), обогащенная логами Watchdog, и поддержка цепочек быстрого исправления (Multi-Fix).
|
||||
2. **HTTP REST Agent API & CLI** — программный интерфейс для внешних ИИ-агентов (Claude, GPT, Open-WebUI, Cursor, AutoGPT и т.д.).
|
||||
3. **Автономный Watchdog** — работающая из коробки система самодиагностики и самовосстановления без внешних зависимостей.
|
||||
|
||||
@@ -9,7 +9,7 @@ Tachyon предоставляет встроенные инструменты
|
||||
|
||||
## 1. AI Doctor (Встроенная LLM-диагностика)
|
||||
|
||||
AI Doctor собирает снимок состояния роутера (проверки sing-box, nftables, DNS, WAN, памяти, uptime и процессов), передаёт его в выбранную LLM и выдаёт краткий вердикт на русском языке. При наличии устранимой проблемы AI Doctor генерирует код быстрого исправления (`FIX: <код>`).
|
||||
AI Doctor собирает снимок состояния роутера (проверки sing-box, nftables, DNS, WAN, памяти, uptime, активные сбои Watchdog и 15 последних критических строк системного лога), передаёт его в выбранную LLM и выдаёт вердикт на русском языке. При наличии проблем AI Doctor генерирует цепочку кодов авто-исправления (`FIX: код1, код2`). Результат сохраняется в `/tmp/ai_doctor_last.json`.
|
||||
|
||||
### 1.1. Настройка AI Doctor
|
||||
|
||||
@@ -19,7 +19,7 @@ AI Doctor собирает снимок состояния роутера (пр
|
||||
# 1. Включить модуль AI Doctor
|
||||
uci set tachyon.settings.enable_ai_doctor='1'
|
||||
|
||||
# 2. Выбрать провадера: openai | anthropic | deepseek | custom
|
||||
# 2. Выбрать провайдера: openai | anthropic | deepseek | custom
|
||||
uci set tachyon.settings.ai_doctor_provider='openai'
|
||||
|
||||
# 3. Выказать модель (опционально - если пустое, используется дефолт провайдера)
|
||||
@@ -37,26 +37,35 @@ uci commit tachyon
|
||||
|
||||
### 1.2. Поддерживаемые провайдеры и модели
|
||||
|
||||
| Провайдер | UCI `ai_doctor_provider` | Модель по умолчанию (если `ai_doctor_model` пустое) | Примеры моделей (`ai_doctor_model`) |
|
||||
| Провайдер | UCI `ai_doctor_provider` | Модель по умолчанию | Примеры моделей (`ai_doctor_model`) |
|
||||
|---|---|---|---|
|
||||
| **OpenAI** | `openai` | `gpt-4o-mini` | `gpt-4o`, `gpt-4.1-mini`, `o4-mini` |
|
||||
| **Anthropic** | `anthropic` | `claude-3-5-haiku-20241022` | `claude-3-5-sonnet-20241022`, `claude-3-7-sonnet-20250219` |
|
||||
| **DeepSeek** | `deepseek` | `deepseek-chat` | `deepseek-reasoner` |
|
||||
| **Custom API** | `custom` | `gpt-4o-mini` | Название любой модели вашего сервера (например `llama-3.3-70b`, `qwen-2.5-72b`) |
|
||||
| **Custom API** | `custom` | `gpt-4o-mini` | Любая модель вашего сервера (`llama-3.3-70b`, `qwen-2.5-72b`) |
|
||||
|
||||
### 1.3. Коды быстрого исправления (Quick Fix Codes)
|
||||
### 1.3. Коды быстрого исправления (Quick Fix Codes — 13 кодов)
|
||||
|
||||
Если LLM находит известную проблему, она возвращает код быстрого исправления. Tachyon поддерживает следующие автоматические исправления:
|
||||
AI Doctor может вернуть один или несколько кодов быстрого исправления через запятую:
|
||||
|
||||
- `start_singbox` — перезапуск упавшего сервиса sing-box
|
||||
- `rebuild_rules` — пересборка и сброс nftables-правил фильтрации
|
||||
- `fix_dnsmasq` — восстановление корректной конфигурации dnsmasq
|
||||
- `fix_resolv_symlink` — исправление повреждённого символического звена `/etc/resolv.conf`
|
||||
- `start_watchdog` — запуск остановленного процесса Watchdog
|
||||
- `restart_singbox_dns` — перезапуск DNS-модуля sing-box при сбое ответов
|
||||
- `fix_uci_config` — восстановление повреждённого UCI-конфига Tachyon из бэкапа
|
||||
- `fix_wan_interface` — перезапуск сбойного сетевого интерфейса WAN
|
||||
- `fix_gateway` — исправление отсутствующего или некорректного системного шлюза
|
||||
1. `start_singbox` — перезапуск упавшего сервиса sing-box
|
||||
2. `rebuild_rules` — пересборка и сброс nftables-правил фильтрации
|
||||
3. `fix_dnsmasq` — перезапуск и восстановление службы dnsmasq
|
||||
4. `fix_resolv_symlink` — исправление повреждённого символического звена `/etc/resolv.conf`
|
||||
5. `start_watchdog` — перезапуск службы Watchdog
|
||||
6. `restart_singbox_dns` — перезапуск DNS-модуля sing-box при сбое ответов
|
||||
7. `fix_uci_config` — восстановление конфигурации Tachyon из бэкапа
|
||||
8. `fix_wan_interface` — перезапуск сетевого интерфейса WAN (`ifup wan`)
|
||||
9. `fix_gateway` — перезапуск сетевого стека для восстановления системного шлюза
|
||||
10. `clear_dns_cache` — очистка кэша DNS и база данных sing-box `cache.db`
|
||||
11. `update_subscriptions` — принудительный запуск обновления прокси-подписок
|
||||
12. `reset_firewall` — полный перезапуск фаервола роутера (`/etc/init.d/firewall restart`)
|
||||
13. `restart_network` — полный перезапуск сетевой службы (`/etc/init.d/network restart`)
|
||||
|
||||
Применение кодов из CLI:
|
||||
```sh
|
||||
tachyon apply_quick_fix clear_dns_cache,start_singbox
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -67,7 +76,9 @@ Watchdog Tachyon работает автономно на роутере каж
|
||||
### Полезные CLI-команды:
|
||||
|
||||
```sh
|
||||
tachyon ai_doctor # запуск анализа AI Doctor (вызывает LLM)
|
||||
tachyon ai_doctor # запуск анализа AI Doctor (вызывает LLM и сохраняет в /tmp/ai_doctor_last.json)
|
||||
tachyon ai_doctor_last # получение последнего сохраненного отчета AI Doctor
|
||||
tachyon apply_quick_fix # выполнение кода(ов) быстрого исправления
|
||||
tachyon ai_heal # ручной запуск полного цикла авто-починки Watchdog
|
||||
tachyon ai_status # краткий JSON-статус Watchdog
|
||||
tachyon ai_status_full # полный JSON-статус с метриками и таймерами
|
||||
@@ -87,7 +98,7 @@ http://<ip-роутера>/cgi-bin/tachyon-agent/<endpoint>
|
||||
|
||||
### 3.1. Настройка токена доступа (Agent API Token)
|
||||
|
||||
WRITE-операции (изменение настроек, перезапуск сервисов) требуют Bearer-авторизации:
|
||||
WRITE-операции (изменение настроек, перезапуск сервисов, применение фиксов) требуют Bearer-авторизации:
|
||||
|
||||
```sh
|
||||
uci set tachyon.settings.agent_api_token='СЕКРЕТНЫЙ_ТОКЕН_АГЕНТА'
|
||||
@@ -101,17 +112,19 @@ uci commit tachyon
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/cgi-bin/tachyon-agent/health` | Быстрая проверка работоспособности служб |
|
||||
| GET | `/cgi-bin/tachyon-agent/snapshot` | Полный сним ок системы для LLM |
|
||||
| GET | `/cgi-bin/tachyon-agent/snapshot` | Полный снимок системы для LLM |
|
||||
| GET | `/cgi-bin/tachyon-agent/diagnose` | Результаты диагностики + предложения по ремонту |
|
||||
| GET | `/cgi-bin/tachyon-agent/logs` | Последние 100 строк системного лога Tachyon |
|
||||
| GET | `/cgi-bin/tachyon-agent/config` | Конфигурация UCI (секреты скрыты) |
|
||||
| GET | `/cgi-bin/tachyon-agent/tools` | Схема инструментов в формате OpenAI Function Calling / MCP |
|
||||
| GET | `/cgi-bin/tachyon-agent/ai-doctor/last` | Последний отчёт AI Doctor с меткой времени |
|
||||
|
||||
#### WRITE (Обязателен заголовок `Authorization: Bearer <токен>`):
|
||||
|
||||
| Метод | Путь | Тело запроса | Описание |
|
||||
|---|---|---|---|
|
||||
| POST | `/cgi-bin/tachyon-agent/heal` | `{}` | Принудительный запуск авто-ремонта |
|
||||
| POST | `/cgi-bin/tachyon-agent/ai-doctor/fix` | `{"fix":"clear_dns_cache,start_singbox"}` | Применение кодов исправления |
|
||||
| POST | `/cgi-bin/tachyon-agent/restart` | `{"service":"singbox"}` | Перезапуск службы (`singbox`, `watchdog`, `dnsmasq`) |
|
||||
| POST | `/cgi-bin/tachyon-agent/reload` | `{}` | Перезагрузка правил и конфигурации |
|
||||
| POST | `/cgi-bin/tachyon-agent/config/set` | `{"section":"settings","option":"dns_type","value":"doh"}` | Изменение параметров UCI |
|
||||
@@ -128,61 +141,34 @@ uci commit tachyon
|
||||
# Проверка здоровья
|
||||
curl http://192.168.1.1/cgi-bin/tachyon-agent/health
|
||||
|
||||
# Полный снимок системы для промпта LLM
|
||||
curl http://192.168.1.1/cgi-bin/tachyon-agent/snapshot | jq .
|
||||
# Получение последнего отчёта AI Doctor
|
||||
curl http://192.168.1.1/cgi-bin/tachyon-agent/ai-doctor/last | jq .
|
||||
|
||||
# Вызов команды восстановления с токеном
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer СЕКРЕТНЫЙ_ТОКЕН_АГЕНТА" \
|
||||
http://192.168.1.1/cgi-bin/tachyon-agent/heal
|
||||
|
||||
# Изменение настроек UCI через API
|
||||
# Применение исправления от ИИ с токеном
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer СЕКРЕТНЫЙ_ТОКЕН_АГЕНТА" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"section":"settings","option":"ai_doctor_model","value":"gpt-4o"}' \
|
||||
http://192.168.1.1/cgi-bin/tachyon-agent/config/set
|
||||
-d '{"fix":"clear_dns_cache,start_singbox"}' \
|
||||
http://192.168.1.1/cgi-bin/tachyon-agent/ai-doctor/fix
|
||||
```
|
||||
|
||||
### 4.2. CLI вызовы через SSH
|
||||
|
||||
Для внешних агентов с SSH-доступом:
|
||||
|
||||
```sh
|
||||
# Структурированный отчёт
|
||||
tachyon diagnose_json
|
||||
# Вызов AI Doctor
|
||||
tachyon ai_doctor
|
||||
|
||||
# Вызов API через внутреннюю утилиту agent
|
||||
tachyon agent /tachyon/agent/v1/health GET
|
||||
tachyon agent /tachyon/agent/v1/snapshot GET
|
||||
tachyon agent /tachyon/agent/v1/heal POST '{}' 'Bearer СЕКРЕТНЫЙ_ТОКЕН_АГЕНТА'
|
||||
# Вызов последнего отчета
|
||||
tachyon ai_doctor_last
|
||||
|
||||
# Выполнение фиксов
|
||||
tachyon apply_quick_fix clear_dns_cache
|
||||
```
|
||||
|
||||
### 4.3. Подключение к Cursor / Claude Desktop / Open-WebUI
|
||||
|
||||
1. **Запросить схему инструментов:**
|
||||
```sh
|
||||
curl http://192.168.1.1/cgi-bin/tachyon-agent/tools
|
||||
```
|
||||
2. **Системный промпт для внешнего ИИ-агента:**
|
||||
```text
|
||||
You are a network administrator AI agent connected to Tachyon OpenWrt router.
|
||||
Base API URL: http://192.168.1.1/cgi-bin/tachyon-agent/
|
||||
Auth Header for POST commands: Authorization: Bearer <YOUR_AGENT_API_TOKEN>
|
||||
|
||||
1. Call GET /snapshot to read the current network state.
|
||||
2. Call GET /diagnose to check system issues.
|
||||
3. Execute actions using POST endpoints provided in GET /tools.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Безопасность
|
||||
|
||||
- **READ-эндпоинты** не передают пароли, приватные ключи подписчиков и бот-токены.
|
||||
- **WRITE-эндпоинты** всегда блокируются без валидного Bearer-токена.
|
||||
- **По умолчанию WAN-доступ к HTTP API закрыт** брандмауэром OpenWrt.
|
||||
- Для удаленного управления рекомендуется использовать SSH-туннелирование:
|
||||
```sh
|
||||
ssh -L 8080:192.168.1.1:80 root@router-ip
|
||||
```
|
||||
- **По умолчанию WAN-доступ к HTTP API закрыт** брандмауэром OpenWrt.
|
||||
@@ -334,6 +334,27 @@ export const TachyonShellMethods = {
|
||||
'/usr/bin/tachyon',
|
||||
{ timeout: 30000 },
|
||||
),
|
||||
aiDoctor: async () =>
|
||||
callBaseMethod<unknown>(
|
||||
Tachyon.AvailableMethods.AI_DOCTOR,
|
||||
[],
|
||||
'/usr/bin/tachyon',
|
||||
{ timeout: 60000 },
|
||||
),
|
||||
aiDoctorLast: async () =>
|
||||
callBaseMethod<unknown>(
|
||||
Tachyon.AvailableMethods.AI_DOCTOR_LAST,
|
||||
[],
|
||||
'/usr/bin/tachyon',
|
||||
{ timeout: 10000 },
|
||||
),
|
||||
applyQuickFix: async (fixCode: string) =>
|
||||
callBaseMethod<unknown>(
|
||||
Tachyon.AvailableMethods.APPLY_QUICK_FIX,
|
||||
[fixCode],
|
||||
'/usr/bin/tachyon',
|
||||
{ timeout: 30000 },
|
||||
),
|
||||
showSingBoxConfig: async (masked = true) =>
|
||||
callBaseMethod<unknown>(Tachyon.AvailableMethods.SHOW_SING_BOX_CONFIG, [
|
||||
masked ? 'masked' : 'raw',
|
||||
|
||||
@@ -185,6 +185,7 @@ export interface StoreType {
|
||||
disable: { loading: boolean };
|
||||
globalCheck: { loading: boolean };
|
||||
doctor: { loading: boolean };
|
||||
aiDoctor: { loading: boolean };
|
||||
viewLogs: { loading: boolean };
|
||||
showSingBoxConfig: { loading: boolean };
|
||||
generateBugReport: { loading: boolean };
|
||||
|
||||
@@ -120,6 +120,9 @@ export const initialDiagnosticStore: Pick<
|
||||
doctor: {
|
||||
loading: false,
|
||||
},
|
||||
aiDoctor: {
|
||||
loading: false,
|
||||
},
|
||||
viewLogs: {
|
||||
loading: false,
|
||||
},
|
||||
|
||||
@@ -764,6 +764,153 @@ async function handleRunDoctor() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRunAiDoctor() {
|
||||
setDiagnosticActionLoading('aiDoctor', true);
|
||||
|
||||
try {
|
||||
const aiRes = await TachyonShellMethods.aiDoctor();
|
||||
|
||||
if (!aiRes || typeof aiRes !== 'object') {
|
||||
showToast(_('AI Doctor failed') + ': ' + _('Unknown error'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const rawData = (aiRes as { data?: unknown }).data;
|
||||
const data =
|
||||
typeof rawData === 'object' && rawData !== null
|
||||
? (rawData as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
if ((aiRes as { success?: boolean }).success || (data && data.success)) {
|
||||
const report = data ? String(data.report ?? '') : String(rawData ?? '');
|
||||
const quickFixes: string[] = Array.isArray(data?.quick_fixes)
|
||||
? (data.quick_fixes as string[])
|
||||
: String(data?.quick_fix ?? '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const title = _('AI Doctor Diagnosis');
|
||||
|
||||
const modalContent = E('div', { class: 'tachyon_ai_doctor_modal' }, [
|
||||
E(
|
||||
'pre',
|
||||
{
|
||||
style:
|
||||
'white-space: pre-wrap; font-family: inherit; background: rgba(0,0,0,0.04); padding: 12px; border-radius: 6px; font-size: 13px; line-height: 1.5;',
|
||||
},
|
||||
report,
|
||||
),
|
||||
quickFixes.length > 0
|
||||
? E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'margin-top: 15px; padding-top: 12px; border-top: 1px solid rgba(0,0,0,0.1);',
|
||||
},
|
||||
[
|
||||
E('b', {}, _('Recommended Quick Fixes:')),
|
||||
E(
|
||||
'div',
|
||||
{
|
||||
style:
|
||||
'display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px;',
|
||||
},
|
||||
[
|
||||
...quickFixes.map((code) =>
|
||||
E(
|
||||
'button',
|
||||
{
|
||||
class: 'btn cbi-button cbi-button-apply',
|
||||
type: 'button',
|
||||
onclick: async () => {
|
||||
showToast(
|
||||
_('Applying fix') + ': ' + code + '...',
|
||||
'success',
|
||||
);
|
||||
const fixRes =
|
||||
await TachyonShellMethods.applyQuickFix(code);
|
||||
if (
|
||||
fixRes &&
|
||||
typeof fixRes === 'object' &&
|
||||
(fixRes as { success?: boolean }).success
|
||||
) {
|
||||
showToast(
|
||||
_('Fix applied') + ': ' + code,
|
||||
'success',
|
||||
);
|
||||
} else {
|
||||
showToast(
|
||||
_('Failed to apply fix') + ': ' + code,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
_('Fix') + ': ' + code,
|
||||
),
|
||||
),
|
||||
quickFixes.length > 1
|
||||
? E(
|
||||
'button',
|
||||
{
|
||||
class: 'btn cbi-button cbi-button-save',
|
||||
type: 'button',
|
||||
onclick: async () => {
|
||||
showToast(_('Applying all fixes...'), 'success');
|
||||
const fixRes =
|
||||
await TachyonShellMethods.applyQuickFix(
|
||||
quickFixes.join(','),
|
||||
);
|
||||
if (
|
||||
fixRes &&
|
||||
typeof fixRes === 'object' &&
|
||||
(fixRes as { success?: boolean }).success
|
||||
) {
|
||||
showToast(
|
||||
_('All fixes applied successfully'),
|
||||
'success',
|
||||
);
|
||||
} else {
|
||||
showToast(_('Some fixes failed'), 'error');
|
||||
}
|
||||
},
|
||||
},
|
||||
_('Fix All'),
|
||||
)
|
||||
: E('span', {}),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: E(
|
||||
'div',
|
||||
{ style: 'margin-top: 10px; color: #2e7d32;' },
|
||||
'✅ ' + _('No quick fix required'),
|
||||
),
|
||||
]);
|
||||
|
||||
ui.showModal(title, modalContent);
|
||||
} else {
|
||||
const errorMsg =
|
||||
typeof (aiRes as { error?: string }).error === 'string'
|
||||
? (aiRes as { error?: string }).error
|
||||
: _('Unknown error');
|
||||
showToast(_('AI Doctor failed') + ': ' + errorMsg, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
'[DIAGNOSTIC]',
|
||||
'handleRunAiDoctor - e',
|
||||
e instanceof Error ? e.message : String(e),
|
||||
);
|
||||
showToast(_('AI Doctor failed'), 'error');
|
||||
} finally {
|
||||
setDiagnosticActionLoading('aiDoctor', false);
|
||||
runChecks();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewLogs() {
|
||||
setDiagnosticActionLoading('viewLogs', true);
|
||||
|
||||
@@ -1007,6 +1154,12 @@ function renderDiagnosticAvailableActionsWidget() {
|
||||
onClick: handleRunDoctor,
|
||||
disabled: utilityActionsDisabled,
|
||||
},
|
||||
aiDoctor: {
|
||||
loading: diagnosticsActions.aiDoctor.loading,
|
||||
visible: true,
|
||||
onClick: handleRunAiDoctor,
|
||||
disabled: utilityActionsDisabled,
|
||||
},
|
||||
viewLogs: {
|
||||
loading: diagnosticsActions.viewLogs.loading,
|
||||
visible: true,
|
||||
|
||||
@@ -27,6 +27,7 @@ interface IRenderAvailableActionsProps {
|
||||
disable: ActionProps;
|
||||
globalCheck: ActionProps;
|
||||
doctor: ActionProps;
|
||||
aiDoctor: ActionProps;
|
||||
viewLogs: ActionProps;
|
||||
showSingBoxConfig: ActionProps;
|
||||
generateBugReport: ActionProps;
|
||||
@@ -40,6 +41,7 @@ export function renderAvailableActions({
|
||||
disable,
|
||||
globalCheck,
|
||||
doctor,
|
||||
aiDoctor,
|
||||
viewLogs,
|
||||
showSingBoxConfig,
|
||||
generateBugReport,
|
||||
@@ -114,6 +116,16 @@ export function renderAvailableActions({
|
||||
disabled: doctor.disabled,
|
||||
}),
|
||||
]),
|
||||
...insertIf(aiDoctor.visible, [
|
||||
renderButton({
|
||||
classNames: ['cbi-button-action'],
|
||||
onClick: aiDoctor.onClick,
|
||||
icon: renderRotateCcwIcon24,
|
||||
text: _('Run AI Doctor'),
|
||||
loading: aiDoctor.loading,
|
||||
disabled: aiDoctor.disabled,
|
||||
}),
|
||||
]),
|
||||
...insertIf(viewLogs.visible, [
|
||||
renderButton({
|
||||
onClick: viewLogs.onClick,
|
||||
|
||||
@@ -73,6 +73,9 @@ export namespace Tachyon {
|
||||
DISABLE = 'disable',
|
||||
GLOBAL_CHECK = 'global_check',
|
||||
DOCTOR = 'doctor',
|
||||
AI_DOCTOR = 'ai_doctor',
|
||||
AI_DOCTOR_LAST = 'ai_doctor_last',
|
||||
APPLY_QUICK_FIX = 'apply_quick_fix',
|
||||
SHOW_SING_BOX_CONFIG = 'show_sing_box_config',
|
||||
CHECK_LOGS = 'check_logs',
|
||||
CHECK_SING_BOX_LOGS = 'check_sing_box_logs',
|
||||
|
||||
@@ -3036,6 +3036,9 @@ var Tachyon;
|
||||
AvailableMethods2["DISABLE"] = "disable";
|
||||
AvailableMethods2["GLOBAL_CHECK"] = "global_check";
|
||||
AvailableMethods2["DOCTOR"] = "doctor";
|
||||
AvailableMethods2["AI_DOCTOR"] = "ai_doctor";
|
||||
AvailableMethods2["AI_DOCTOR_LAST"] = "ai_doctor_last";
|
||||
AvailableMethods2["APPLY_QUICK_FIX"] = "apply_quick_fix";
|
||||
AvailableMethods2["SHOW_SING_BOX_CONFIG"] = "show_sing_box_config";
|
||||
AvailableMethods2["CHECK_LOGS"] = "check_logs";
|
||||
AvailableMethods2["CHECK_SING_BOX_LOGS"] = "check_sing_box_logs";
|
||||
@@ -3333,6 +3336,24 @@ var TachyonShellMethods = {
|
||||
"/usr/bin/tachyon",
|
||||
{ timeout: 3e4 }
|
||||
),
|
||||
aiDoctor: async () => callBaseMethod(
|
||||
Tachyon.AvailableMethods.AI_DOCTOR,
|
||||
[],
|
||||
"/usr/bin/tachyon",
|
||||
{ timeout: 6e4 }
|
||||
),
|
||||
aiDoctorLast: async () => callBaseMethod(
|
||||
Tachyon.AvailableMethods.AI_DOCTOR_LAST,
|
||||
[],
|
||||
"/usr/bin/tachyon",
|
||||
{ timeout: 1e4 }
|
||||
),
|
||||
applyQuickFix: async (fixCode) => callBaseMethod(
|
||||
Tachyon.AvailableMethods.APPLY_QUICK_FIX,
|
||||
[fixCode],
|
||||
"/usr/bin/tachyon",
|
||||
{ timeout: 3e4 }
|
||||
),
|
||||
showSingBoxConfig: async (masked = true) => callBaseMethod(Tachyon.AvailableMethods.SHOW_SING_BOX_CONFIG, [
|
||||
masked ? "masked" : "raw"
|
||||
]),
|
||||
@@ -5053,6 +5074,9 @@ var initialDiagnosticStore = {
|
||||
doctor: {
|
||||
loading: false
|
||||
},
|
||||
aiDoctor: {
|
||||
loading: false
|
||||
},
|
||||
viewLogs: {
|
||||
loading: false
|
||||
},
|
||||
@@ -9693,6 +9717,7 @@ function renderAvailableActions({
|
||||
disable,
|
||||
globalCheck,
|
||||
doctor,
|
||||
aiDoctor,
|
||||
viewLogs,
|
||||
showSingBoxConfig,
|
||||
generateBugReport
|
||||
@@ -9767,6 +9792,16 @@ function renderAvailableActions({
|
||||
disabled: doctor.disabled
|
||||
})
|
||||
]),
|
||||
...insertIf(aiDoctor.visible, [
|
||||
renderButton({
|
||||
classNames: ["cbi-button-action"],
|
||||
onClick: aiDoctor.onClick,
|
||||
icon: renderRotateCcwIcon24,
|
||||
text: _("Run AI Doctor"),
|
||||
loading: aiDoctor.loading,
|
||||
disabled: aiDoctor.disabled
|
||||
})
|
||||
]),
|
||||
...insertIf(viewLogs.visible, [
|
||||
renderButton({
|
||||
onClick: viewLogs.onClick,
|
||||
@@ -11088,6 +11123,117 @@ async function handleRunDoctor() {
|
||||
runChecks();
|
||||
}
|
||||
}
|
||||
async function handleRunAiDoctor() {
|
||||
setDiagnosticActionLoading("aiDoctor", true);
|
||||
try {
|
||||
const aiRes = await TachyonShellMethods.aiDoctor();
|
||||
if (!aiRes || typeof aiRes !== "object") {
|
||||
showToast(_("AI Doctor failed") + ": " + _("Unknown error"), "error");
|
||||
return;
|
||||
}
|
||||
const rawData = aiRes.data;
|
||||
const data = typeof rawData === "object" && rawData !== null ? rawData : null;
|
||||
if (aiRes.success || data && data.success) {
|
||||
const report = data ? String(data.report ?? "") : String(rawData ?? "");
|
||||
const quickFixes = Array.isArray(data?.quick_fixes) ? data.quick_fixes : String(data?.quick_fix ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
||||
const title = _("AI Doctor Diagnosis");
|
||||
const modalContent = E("div", { class: "tachyon_ai_doctor_modal" }, [
|
||||
E(
|
||||
"pre",
|
||||
{
|
||||
style: "white-space: pre-wrap; font-family: inherit; background: rgba(0,0,0,0.04); padding: 12px; border-radius: 6px; font-size: 13px; line-height: 1.5;"
|
||||
},
|
||||
report
|
||||
),
|
||||
quickFixes.length > 0 ? E(
|
||||
"div",
|
||||
{
|
||||
style: "margin-top: 15px; padding-top: 12px; border-top: 1px solid rgba(0,0,0,0.1);"
|
||||
},
|
||||
[
|
||||
E("b", {}, _("Recommended Quick Fixes:")),
|
||||
E(
|
||||
"div",
|
||||
{
|
||||
style: "display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px;"
|
||||
},
|
||||
[
|
||||
...quickFixes.map(
|
||||
(code) => E(
|
||||
"button",
|
||||
{
|
||||
class: "btn cbi-button cbi-button-apply",
|
||||
type: "button",
|
||||
onclick: async () => {
|
||||
showToast(
|
||||
_("Applying fix") + ": " + code + "...",
|
||||
"success"
|
||||
);
|
||||
const fixRes = await TachyonShellMethods.applyQuickFix(code);
|
||||
if (fixRes && typeof fixRes === "object" && fixRes.success) {
|
||||
showToast(
|
||||
_("Fix applied") + ": " + code,
|
||||
"success"
|
||||
);
|
||||
} else {
|
||||
showToast(
|
||||
_("Failed to apply fix") + ": " + code,
|
||||
"error"
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
_("Fix") + ": " + code
|
||||
)
|
||||
),
|
||||
quickFixes.length > 1 ? E(
|
||||
"button",
|
||||
{
|
||||
class: "btn cbi-button cbi-button-save",
|
||||
type: "button",
|
||||
onclick: async () => {
|
||||
showToast(_("Applying all fixes..."), "success");
|
||||
const fixRes = await TachyonShellMethods.applyQuickFix(
|
||||
quickFixes.join(",")
|
||||
);
|
||||
if (fixRes && typeof fixRes === "object" && fixRes.success) {
|
||||
showToast(
|
||||
_("All fixes applied successfully"),
|
||||
"success"
|
||||
);
|
||||
} else {
|
||||
showToast(_("Some fixes failed"), "error");
|
||||
}
|
||||
}
|
||||
},
|
||||
_("Fix All")
|
||||
) : E("span", {})
|
||||
]
|
||||
)
|
||||
]
|
||||
) : E(
|
||||
"div",
|
||||
{ style: "margin-top: 10px; color: #2e7d32;" },
|
||||
"\u2705 " + _("No quick fix required")
|
||||
)
|
||||
]);
|
||||
ui.showModal(title, modalContent);
|
||||
} else {
|
||||
const errorMsg = typeof aiRes.error === "string" ? aiRes.error : _("Unknown error");
|
||||
showToast(_("AI Doctor failed") + ": " + errorMsg, "error");
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
"[DIAGNOSTIC]",
|
||||
"handleRunAiDoctor - e",
|
||||
e instanceof Error ? e.message : String(e)
|
||||
);
|
||||
showToast(_("AI Doctor failed"), "error");
|
||||
} finally {
|
||||
setDiagnosticActionLoading("aiDoctor", false);
|
||||
runChecks();
|
||||
}
|
||||
}
|
||||
async function handleViewLogs() {
|
||||
setDiagnosticActionLoading("viewLogs", true);
|
||||
try {
|
||||
@@ -11290,6 +11436,12 @@ function renderDiagnosticAvailableActionsWidget() {
|
||||
onClick: handleRunDoctor,
|
||||
disabled: utilityActionsDisabled
|
||||
},
|
||||
aiDoctor: {
|
||||
loading: diagnosticsActions.aiDoctor.loading,
|
||||
visible: true,
|
||||
onClick: handleRunAiDoctor,
|
||||
disabled: utilityActionsDisabled
|
||||
},
|
||||
viewLogs: {
|
||||
loading: diagnosticsActions.viewLogs.loading,
|
||||
visible: true,
|
||||
|
||||
@@ -219,6 +219,8 @@ function command_spec(command) {
|
||||
global_check: [ "diagnostics/runtime.uc", "global-check", 2 ],
|
||||
doctor: [ "diagnostics/runtime.uc", "doctor", 1 ],
|
||||
ai_doctor: [ "diagnostics/runtime.uc", "ai-doctor", 0 ],
|
||||
ai_doctor_last: [ "diagnostics/runtime.uc", "ai-doctor-last", 0 ],
|
||||
apply_quick_fix: [ "diagnostics/runtime.uc", "apply-quick-fix", 1 ],
|
||||
diagnose_json: [ "diagnostics/runtime.uc", "diagnose-json", 0 ],
|
||||
agent_cgi: [ "service/agent_api.uc", "__cgi", 0 ],
|
||||
watchdog: [ "service/watchdog.uc", "", 1 ],
|
||||
|
||||
@@ -2941,6 +2941,113 @@ function diagnose_json() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function apply_quick_fix(codes_str) {
|
||||
if (!codes_str || codes_str == "") {
|
||||
print(sprintf("%J\n", { success: false, error: "No fix code provided" }));
|
||||
return 1;
|
||||
}
|
||||
|
||||
let codes = split(replace(codes_str, /[ \[\]"]/g, ""), ",");
|
||||
let results = [];
|
||||
let all_ok = true;
|
||||
|
||||
for (let c in codes) {
|
||||
c = trim(c);
|
||||
if (c == "") continue;
|
||||
|
||||
let status = false;
|
||||
let msg = "";
|
||||
|
||||
if (c == "start_singbox") {
|
||||
command_status("/etc/init.d/sing-box restart >/dev/null 2>&1");
|
||||
status = true;
|
||||
msg = "sing-box restarted";
|
||||
} else if (c == "rebuild_rules") {
|
||||
command_status("/etc/init.d/tachyon reload >/dev/null 2>&1");
|
||||
status = true;
|
||||
msg = "Firewall rules rebuilt";
|
||||
} else if (c == "fix_dnsmasq") {
|
||||
command_status("/etc/init.d/dnsmasq restart >/dev/null 2>&1");
|
||||
status = true;
|
||||
msg = "dnsmasq restarted";
|
||||
} else if (c == "fix_resolv_symlink") {
|
||||
command_status("ln -sf /tmp/resolv.conf.auto /etc/resolv.conf");
|
||||
status = true;
|
||||
msg = "resolv.conf symlink fixed";
|
||||
} else if (c == "start_watchdog") {
|
||||
command_status("/etc/init.d/tachyon restart >/dev/null 2>&1");
|
||||
status = true;
|
||||
msg = "Watchdog started";
|
||||
} else if (c == "restart_singbox_dns") {
|
||||
command_status("/etc/init.d/sing-box restart >/dev/null 2>&1");
|
||||
status = true;
|
||||
msg = "sing-box DNS restarted";
|
||||
} else if (c == "fix_uci_config") {
|
||||
command_status("cp /etc/config/tachyon.bak /etc/config/tachyon 2>/dev/null");
|
||||
status = true;
|
||||
msg = "UCI config restored from backup";
|
||||
} else if (c == "fix_wan_interface") {
|
||||
command_status("ifup wan 2>/dev/null");
|
||||
status = true;
|
||||
msg = "WAN interface re-up triggered";
|
||||
} else if (c == "fix_gateway") {
|
||||
command_status("/etc/init.d/network restart >/dev/null 2>&1");
|
||||
status = true;
|
||||
msg = "Network restarted to resolve gateway";
|
||||
} else if (c == "clear_dns_cache") {
|
||||
command_status("/etc/init.d/dnsmasq restart >/dev/null 2>&1");
|
||||
command_status("rm -f /etc/sing-box/cache.db 2>/dev/null");
|
||||
status = true;
|
||||
msg = "DNS cache cleared and dnsmasq restarted";
|
||||
} else if (c == "update_subscriptions") {
|
||||
command_status("/usr/bin/tachyon component_action update_subscriptions >/dev/null 2>&1 &");
|
||||
status = true;
|
||||
msg = "Subscription update triggered";
|
||||
} else if (c == "reset_firewall") {
|
||||
command_status("/etc/init.d/firewall restart >/dev/null 2>&1");
|
||||
status = true;
|
||||
msg = "Firewall restarted";
|
||||
} else if (c == "restart_network") {
|
||||
command_status("/etc/init.d/network restart >/dev/null 2>&1");
|
||||
status = true;
|
||||
msg = "Network service restarted";
|
||||
} else {
|
||||
status = false;
|
||||
msg = "Unknown fix code: " + c;
|
||||
all_ok = false;
|
||||
}
|
||||
|
||||
push(results, { code: c, success: status, message: msg });
|
||||
}
|
||||
|
||||
print(sprintf("%J\n", {
|
||||
success: all_ok,
|
||||
results: results
|
||||
}));
|
||||
return all_ok ? 0 : 1;
|
||||
}
|
||||
|
||||
function ai_doctor_last() {
|
||||
let raw = fs.readfile("/tmp/ai_doctor_last.json");
|
||||
if (!raw) {
|
||||
print(sprintf("%J\n", {
|
||||
success: false,
|
||||
error: "No previous AI Doctor report found"
|
||||
}));
|
||||
return 0;
|
||||
}
|
||||
let data = parse_json_or_null(raw);
|
||||
if (!data) {
|
||||
print(sprintf("%J\n", {
|
||||
success: false,
|
||||
error: "Corrupted AI Doctor history file"
|
||||
}));
|
||||
return 0;
|
||||
}
|
||||
print(sprintf("%J\n", data));
|
||||
return 0;
|
||||
}
|
||||
|
||||
function ai_doctor() {
|
||||
let cfg = uci_settings();
|
||||
if (cfg.enable_ai_doctor != "1" || !cfg.ai_doctor_api_key) {
|
||||
@@ -2960,15 +3067,58 @@ function ai_doctor() {
|
||||
let uptime_out = trim(command_output("cat /proc/uptime 2>/dev/null"));
|
||||
let uptime_min = uptime_out != "" ? int(split(uptime_out, ".")[0]) / 60 : 0;
|
||||
|
||||
let watchdog_status_raw = fs.readfile("/tmp/tachyon_ai_status.json");
|
||||
let watchdog_info = "Watchdog Status: unavailable";
|
||||
if (watchdog_status_raw) {
|
||||
let wd_json = parse_json_or_null(watchdog_status_raw);
|
||||
if (wd_json) {
|
||||
watchdog_info = sprintf(
|
||||
"Watchdog Status:\n- Last OOM: %s\n- WAN fail streak: %d\n- Proxy fail streak: %d\n- DNS fail streak: %d\n- Active repairs: %s",
|
||||
as_string(wd_json.last_oom_time || "none"),
|
||||
int(wd_json.wan_fail_streak || 0),
|
||||
int(wd_json.proxy_fail_streak || 0),
|
||||
int(wd_json.dns_fail_streak || 0),
|
||||
as_string(wd_json.active_repairs || "none")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let log_snippet = trim(command_output("logread | grep -iE 'tachyon|sing-box|dnsmasq|oom|error|fatal' | tail -n 15 2>/dev/null")) || "No recent system errors logged.";
|
||||
|
||||
let sys_context = sprintf(
|
||||
"Tachyon Doctor Report:\n%s\n" +
|
||||
"Tachyon Doctor Report:\n%s\n\n" +
|
||||
"System Parameters:\n" +
|
||||
"Version: %s\n" +
|
||||
"DNS type: %s\n" +
|
||||
"sing-box running: %s\n" +
|
||||
"Uptime: %d minutes\n",
|
||||
report, version, dns_type, singbox_running ? "yes" : "no", uptime_min
|
||||
"Uptime: %d minutes\n\n" +
|
||||
"%s\n\n" +
|
||||
"Recent Critical System Logs:\n%s\n",
|
||||
report, version, dns_type, singbox_running ? "yes" : "no", uptime_min,
|
||||
watchdog_info, log_snippet
|
||||
);
|
||||
let prompt = sprintf("Вы — ИИ-ассистент \"Tachyon AI Doctor\" для сервиса обхода блокировок на OpenWrt.\nПроанализируйте диагностический отчет ниже.\n\n%s\n\nСформулируйте краткий диагноз (на русском, максимум 3 пункта).\nЕсли авто-исправление возможно, укажите в конце:\nFIX: <код>\n\nДоступные коды:\n- start_singbox (sing-box упал)\n- rebuild_rules (nftables нарушены)\n- fix_dnsmasq (конфиг dnsmasq)\n- fix_resolv_symlink (resolv.conf повреждён)\n- start_watchdog (watchdog остановлен)\n- restart_singbox_dns (sing-box DNS не отвечает)\n- fix_uci_config (конфиг Tachyon повреждён)\n- fix_wan_interface (WAN интерфейс не работает)\n- fix_gateway (шлюз отсутствует)\n\nЕсли авто-исправление не применимо, не пишите тег FIX.", sys_context);
|
||||
|
||||
let prompt = sprintf(
|
||||
"Вы — ИИ-ассистент \"Tachyon AI Doctor\" для сервиса обхода блокировок на OpenWrt.\n" +
|
||||
"Проанализируйте диагностический отчет и логи ниже.\n\n%s\n\n" +
|
||||
"Сформулируйте краткий диагноз (на русском, максимум 3-4 пункта).\n" +
|
||||
"Если авто-исправление возможно, укажите в самом конце ответа строчку:\n" +
|
||||
"FIX: код1, код2\n\n" +
|
||||
"Доступные коды быстрого исправления:\n" +
|
||||
"- start_singbox (sing-box упал или остановлен)\n" +
|
||||
"- rebuild_rules (nftables или ip rule правила нарушены)\n" +
|
||||
"- fix_dnsmasq (конфиг или сервис dnsmasq не отвечает)\n" +
|
||||
"- fix_resolv_symlink (resolv.conf повреждён)\n" +
|
||||
"- start_watchdog (watchdog остановлен)\n" +
|
||||
"- restart_singbox_dns (sing-box DNS не отвечает)\n" +
|
||||
"- fix_uci_config (конфиг Tachyon повреждён)\n" +
|
||||
"- fix_wan_interface (WAN интерфейс не работает)\n" +
|
||||
"- fix_gateway (шлюз отсутствует)\n" +
|
||||
"- clear_dns_cache (очистить кэш DNS и перезапустить dnsmasq)\n" +
|
||||
"- update_subscriptions (принудительно обновить прокси подписки)\n" +
|
||||
"- reset_firewall (перезапустить файрвол роутера)\n" +
|
||||
"- restart_network (перезапустить сетевой стек)\n\n" +
|
||||
"Если авто-исправление не применимо, не пишите тег FIX.", sys_context);
|
||||
|
||||
let provider = cfg.ai_doctor_provider || "openai";
|
||||
let api_key = cfg.ai_doctor_api_key || "";
|
||||
@@ -2984,23 +3134,36 @@ function ai_doctor() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let quick_fix = "";
|
||||
let quick_fixes = [];
|
||||
let text = ai_res;
|
||||
let fix_idx = index(text, "FIX:");
|
||||
if (fix_idx >= 0) {
|
||||
let fix_part = trim(substr(text, fix_idx + 4));
|
||||
let fields = split(fix_part, /[ \t\r\n]+/);
|
||||
if (length(fields) > 0) {
|
||||
quick_fix = replace(fields[0], /[._`*]/g, "");
|
||||
let raw_codes = split(fix_part, /[,\r\n]+/);
|
||||
for (let code in raw_codes) {
|
||||
let clean_code = trim(replace(code, /[._`*]/g, ""));
|
||||
let first_token = split(clean_code, /[ \t]+/)[0];
|
||||
if (first_token != "") {
|
||||
push(quick_fixes, first_token);
|
||||
}
|
||||
}
|
||||
text = trim(substr(text, 0, fix_idx));
|
||||
}
|
||||
let quick_fix = length(quick_fixes) > 0 ? join(",", quick_fixes) : "";
|
||||
|
||||
print(sprintf("%J\n", {
|
||||
let doctor_res = {
|
||||
success: true,
|
||||
timestamp: time(),
|
||||
report: text,
|
||||
quick_fix: quick_fix
|
||||
}));
|
||||
quick_fix: quick_fix,
|
||||
quick_fixes: quick_fixes,
|
||||
provider: provider,
|
||||
model: model_override != "" ? model_override : "default"
|
||||
};
|
||||
|
||||
common.write_json_file("/tmp/ai_doctor_last.json", doctor_res);
|
||||
|
||||
print(sprintf("%J\n", doctor_res));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -3185,6 +3348,10 @@ else if (mode == "diagnose-json")
|
||||
exit(diagnose_json());
|
||||
else if (mode == "ai-doctor")
|
||||
exit(ai_doctor());
|
||||
else if (mode == "ai-doctor-last")
|
||||
exit(ai_doctor_last());
|
||||
else if (mode == "apply-quick-fix")
|
||||
exit(apply_quick_fix(ARGV[1] || ""));
|
||||
else if (mode == "validate-nfqws-strategy-json")
|
||||
exit(validate_nfqws_strategy_json(ARGV[1] || ""));
|
||||
else if (mode == "validate-nfqws2-strategy-json")
|
||||
|
||||
@@ -446,6 +446,39 @@ function handle_domain_add(body) {
|
||||
ok({ message: "Domain '" + domain + "' added to section '" + section + "'" });
|
||||
}
|
||||
|
||||
function handle_ai_doctor_last() {
|
||||
let raw = fs.readfile("/tmp/ai_doctor_last.json");
|
||||
if (!raw) {
|
||||
err("No previous AI Doctor report found", 404);
|
||||
return;
|
||||
}
|
||||
let data = parse_json_safe(raw);
|
||||
if (!data) {
|
||||
err("Corrupted AI Doctor history file", 500);
|
||||
return;
|
||||
}
|
||||
ok(data);
|
||||
}
|
||||
|
||||
function handle_ai_doctor_fix(body) {
|
||||
let fix_code = as_string(body.fix || "");
|
||||
if (fix_code == "" && type(body.fixes) == "array") {
|
||||
fix_code = join(",", body.fixes);
|
||||
}
|
||||
if (fix_code == "") {
|
||||
err("fix (code string) or fixes (string array) is required");
|
||||
return;
|
||||
}
|
||||
|
||||
let res = command_capture("/usr/bin/tachyon apply_quick_fix " + shell_quote(fix_code));
|
||||
let parsed = parse_json_safe(res.output);
|
||||
if (parsed) {
|
||||
write_json(parsed);
|
||||
} else {
|
||||
ok({ message: "Quick fix command executed", code: fix_code });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Dispatcher ───────────────────────────────────────────────────────────────
|
||||
|
||||
let path_arg = as_string(ARGV[0] || "");
|
||||
@@ -469,6 +502,8 @@ if (method == "GET") {
|
||||
handle_config();
|
||||
else if (route == "/tools" || route == "/tools/")
|
||||
handle_tools();
|
||||
else if (route == "/ai-doctor/last" || route == "/ai-doctor/last/")
|
||||
handle_ai_doctor_last();
|
||||
else
|
||||
err("Unknown endpoint: GET " + route, 404);
|
||||
} else if (method == "POST") {
|
||||
@@ -487,6 +522,8 @@ if (method == "GET") {
|
||||
handle_section_toggle(body);
|
||||
else if (route == "/domain/add")
|
||||
handle_domain_add(body);
|
||||
else if (route == "/ai-doctor/fix" || route == "/ai-doctor/fix/")
|
||||
handle_ai_doctor_fix(body);
|
||||
else
|
||||
err("Unknown endpoint: POST " + route, 404);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user