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:
Dushnilin
2026-08-11 16:06:19 +03:00
parent 4a6b24ef28
commit 6dd085b777
11 changed files with 607 additions and 70 deletions
+45 -59
View File
@@ -1,7 +1,7 @@
# Tachyon AI & Agent API Integration # Tachyon AI & Agent API Integration
Tachyon предоставляет встроенные инструменты интеграции с искусственным интеллектом: 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 и т.д.). 2. **HTTP REST Agent API & CLI** — программный интерфейс для внешних ИИ-агентов (Claude, GPT, Open-WebUI, Cursor, AutoGPT и т.д.).
3. **Автономный Watchdog** — работающая из коробки система самодиагностики и самовосстановления без внешних зависимостей. 3. **Автономный Watchdog** — работающая из коробки система самодиагностики и самовосстановления без внешних зависимостей.
@@ -9,7 +9,7 @@ Tachyon предоставляет встроенные инструменты
## 1. AI Doctor (Встроенная LLM-диагностика) ## 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 ### 1.1. Настройка AI Doctor
@@ -19,7 +19,7 @@ AI Doctor собирает снимок состояния роутера (пр
# 1. Включить модуль AI Doctor # 1. Включить модуль AI Doctor
uci set tachyon.settings.enable_ai_doctor='1' 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' uci set tachyon.settings.ai_doctor_provider='openai'
# 3. Выказать модель (опционально - если пустое, используется дефолт провайдера) # 3. Выказать модель (опционально - если пустое, используется дефолт провайдера)
@@ -37,26 +37,35 @@ uci commit tachyon
### 1.2. Поддерживаемые провайдеры и модели ### 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` | | **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` | | **Anthropic** | `anthropic` | `claude-3-5-haiku-20241022` | `claude-3-5-sonnet-20241022`, `claude-3-7-sonnet-20250219` |
| **DeepSeek** | `deepseek` | `deepseek-chat` | `deepseek-reasoner` | | **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 1. `start_singbox` — перезапуск упавшего сервиса sing-box
- `rebuild_rules` — пересборка и сброс nftables-правил фильтрации 2. `rebuild_rules` — пересборка и сброс nftables-правил фильтрации
- `fix_dnsmasq` — восстановление корректной конфигурации dnsmasq 3. `fix_dnsmasq` — перезапуск и восстановление службы dnsmasq
- `fix_resolv_symlink` — исправление повреждённого символического звена `/etc/resolv.conf` 4. `fix_resolv_symlink` — исправление повреждённого символического звена `/etc/resolv.conf`
- `start_watchdog` — запуск остановленного процесса Watchdog 5. `start_watchdog` — перезапуск службы Watchdog
- `restart_singbox_dns` — перезапуск DNS-модуля sing-box при сбое ответов 6. `restart_singbox_dns` — перезапуск DNS-модуля sing-box при сбое ответов
- `fix_uci_config` — восстановление повреждённого UCI-конфига Tachyon из бэкапа 7. `fix_uci_config` — восстановление конфигурации Tachyon из бэкапа
- `fix_wan_interface` — перезапуск сбойного сетевого интерфейса WAN 8. `fix_wan_interface` — перезапуск сетевого интерфейса WAN (`ifup wan`)
- `fix_gateway` — исправление отсутствующего или некорректного системного шлюза 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-команды: ### Полезные CLI-команды:
```sh ```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_heal # ручной запуск полного цикла авто-починки Watchdog
tachyon ai_status # краткий JSON-статус Watchdog tachyon ai_status # краткий JSON-статус Watchdog
tachyon ai_status_full # полный JSON-статус с метриками и таймерами tachyon ai_status_full # полный JSON-статус с метриками и таймерами
@@ -87,7 +98,7 @@ http://<ip-роутера>/cgi-bin/tachyon-agent/<endpoint>
### 3.1. Настройка токена доступа (Agent API Token) ### 3.1. Настройка токена доступа (Agent API Token)
WRITE-операции (изменение настроек, перезапуск сервисов) требуют Bearer-авторизации: WRITE-операции (изменение настроек, перезапуск сервисов, применение фиксов) требуют Bearer-авторизации:
```sh ```sh
uci set tachyon.settings.agent_api_token='СЕКРЕТНЫЙ_ТОКЕН_АГЕНТА' 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/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/diagnose` | Результаты диагностики + предложения по ремонту |
| GET | `/cgi-bin/tachyon-agent/logs` | Последние 100 строк системного лога Tachyon | | GET | `/cgi-bin/tachyon-agent/logs` | Последние 100 строк системного лога Tachyon |
| GET | `/cgi-bin/tachyon-agent/config` | Конфигурация UCI (секреты скрыты) | | GET | `/cgi-bin/tachyon-agent/config` | Конфигурация UCI (секреты скрыты) |
| GET | `/cgi-bin/tachyon-agent/tools` | Схема инструментов в формате OpenAI Function Calling / MCP | | GET | `/cgi-bin/tachyon-agent/tools` | Схема инструментов в формате OpenAI Function Calling / MCP |
| GET | `/cgi-bin/tachyon-agent/ai-doctor/last` | Последний отчёт AI Doctor с меткой времени |
#### WRITE (Обязателен заголовок `Authorization: Bearer <токен>`): #### WRITE (Обязателен заголовок `Authorization: Bearer <токен>`):
| Метод | Путь | Тело запроса | Описание | | Метод | Путь | Тело запроса | Описание |
|---|---|---|---| |---|---|---|---|
| POST | `/cgi-bin/tachyon-agent/heal` | `{}` | Принудительный запуск авто-ремонта | | 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/restart` | `{"service":"singbox"}` | Перезапуск службы (`singbox`, `watchdog`, `dnsmasq`) |
| POST | `/cgi-bin/tachyon-agent/reload` | `{}` | Перезагрузка правил и конфигурации | | POST | `/cgi-bin/tachyon-agent/reload` | `{}` | Перезагрузка правил и конфигурации |
| POST | `/cgi-bin/tachyon-agent/config/set` | `{"section":"settings","option":"dns_type","value":"doh"}` | Изменение параметров UCI | | 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 curl http://192.168.1.1/cgi-bin/tachyon-agent/health
# Полный снимок системы для промпта LLM # Получение последнего отчёта AI Doctor
curl http://192.168.1.1/cgi-bin/tachyon-agent/snapshot | jq . 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 \ curl -X POST \
-H "Authorization: Bearer СЕКРЕТНЫЙ_ТОКЕН_АГЕНТА" \ -H "Authorization: Bearer СЕКРЕТНЫЙ_ТОКЕН_АГЕНТА" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{"section":"settings","option":"ai_doctor_model","value":"gpt-4o"}' \ -d '{"fix":"clear_dns_cache,start_singbox"}' \
http://192.168.1.1/cgi-bin/tachyon-agent/config/set http://192.168.1.1/cgi-bin/tachyon-agent/ai-doctor/fix
``` ```
### 4.2. CLI вызовы через SSH ### 4.2. CLI вызовы через SSH
Для внешних агентов с SSH-доступом:
```sh ```sh
# Структурированный отчёт # Вызов AI Doctor
tachyon diagnose_json tachyon ai_doctor
# Вызов API через внутреннюю утилиту agent # Вызов последнего отчета
tachyon agent /tachyon/agent/v1/health GET tachyon ai_doctor_last
tachyon agent /tachyon/agent/v1/snapshot GET
tachyon agent /tachyon/agent/v1/heal POST '{}' 'Bearer СЕКРЕТНЫЙ_ТОКЕН_АГЕНТА' # Выполнение фиксов
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. Безопасность ## 5. Безопасность
- **READ-эндпоинты** не передают пароли, приватные ключи подписчиков и бот-токены. - **READ-эндпоинты** не передают пароли, приватные ключи подписчиков и бот-токены.
- **WRITE-эндпоинты** всегда блокируются без валидного Bearer-токена. - **WRITE-эндпоинты** всегда блокируются без валидного Bearer-токена.
- **По умолчанию WAN-доступ к HTTP API закрыт** брандмауэром OpenWrt. - **По умолчанию WAN-доступ к HTTP API закрыт** брандмауэром OpenWrt.
- Для удаленного управления рекомендуется использовать SSH-туннелирование:
```sh
ssh -L 8080:192.168.1.1:80 root@router-ip
```
@@ -334,6 +334,27 @@ export const TachyonShellMethods = {
'/usr/bin/tachyon', '/usr/bin/tachyon',
{ timeout: 30000 }, { 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) => showSingBoxConfig: async (masked = true) =>
callBaseMethod<unknown>(Tachyon.AvailableMethods.SHOW_SING_BOX_CONFIG, [ callBaseMethod<unknown>(Tachyon.AvailableMethods.SHOW_SING_BOX_CONFIG, [
masked ? 'masked' : 'raw', masked ? 'masked' : 'raw',
@@ -185,6 +185,7 @@ export interface StoreType {
disable: { loading: boolean }; disable: { loading: boolean };
globalCheck: { loading: boolean }; globalCheck: { loading: boolean };
doctor: { loading: boolean }; doctor: { loading: boolean };
aiDoctor: { loading: boolean };
viewLogs: { loading: boolean }; viewLogs: { loading: boolean };
showSingBoxConfig: { loading: boolean }; showSingBoxConfig: { loading: boolean };
generateBugReport: { loading: boolean }; generateBugReport: { loading: boolean };
@@ -120,6 +120,9 @@ export const initialDiagnosticStore: Pick<
doctor: { doctor: {
loading: false, loading: false,
}, },
aiDoctor: {
loading: false,
},
viewLogs: { viewLogs: {
loading: false, 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() { async function handleViewLogs() {
setDiagnosticActionLoading('viewLogs', true); setDiagnosticActionLoading('viewLogs', true);
@@ -1007,6 +1154,12 @@ function renderDiagnosticAvailableActionsWidget() {
onClick: handleRunDoctor, onClick: handleRunDoctor,
disabled: utilityActionsDisabled, disabled: utilityActionsDisabled,
}, },
aiDoctor: {
loading: diagnosticsActions.aiDoctor.loading,
visible: true,
onClick: handleRunAiDoctor,
disabled: utilityActionsDisabled,
},
viewLogs: { viewLogs: {
loading: diagnosticsActions.viewLogs.loading, loading: diagnosticsActions.viewLogs.loading,
visible: true, visible: true,
@@ -27,6 +27,7 @@ interface IRenderAvailableActionsProps {
disable: ActionProps; disable: ActionProps;
globalCheck: ActionProps; globalCheck: ActionProps;
doctor: ActionProps; doctor: ActionProps;
aiDoctor: ActionProps;
viewLogs: ActionProps; viewLogs: ActionProps;
showSingBoxConfig: ActionProps; showSingBoxConfig: ActionProps;
generateBugReport: ActionProps; generateBugReport: ActionProps;
@@ -40,6 +41,7 @@ export function renderAvailableActions({
disable, disable,
globalCheck, globalCheck,
doctor, doctor,
aiDoctor,
viewLogs, viewLogs,
showSingBoxConfig, showSingBoxConfig,
generateBugReport, generateBugReport,
@@ -114,6 +116,16 @@ export function renderAvailableActions({
disabled: doctor.disabled, 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, [ ...insertIf(viewLogs.visible, [
renderButton({ renderButton({
onClick: viewLogs.onClick, onClick: viewLogs.onClick,
+3
View File
@@ -73,6 +73,9 @@ export namespace Tachyon {
DISABLE = 'disable', DISABLE = 'disable',
GLOBAL_CHECK = 'global_check', GLOBAL_CHECK = 'global_check',
DOCTOR = 'doctor', 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', SHOW_SING_BOX_CONFIG = 'show_sing_box_config',
CHECK_LOGS = 'check_logs', CHECK_LOGS = 'check_logs',
CHECK_SING_BOX_LOGS = 'check_sing_box_logs', CHECK_SING_BOX_LOGS = 'check_sing_box_logs',
@@ -3036,6 +3036,9 @@ var Tachyon;
AvailableMethods2["DISABLE"] = "disable"; AvailableMethods2["DISABLE"] = "disable";
AvailableMethods2["GLOBAL_CHECK"] = "global_check"; AvailableMethods2["GLOBAL_CHECK"] = "global_check";
AvailableMethods2["DOCTOR"] = "doctor"; 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["SHOW_SING_BOX_CONFIG"] = "show_sing_box_config";
AvailableMethods2["CHECK_LOGS"] = "check_logs"; AvailableMethods2["CHECK_LOGS"] = "check_logs";
AvailableMethods2["CHECK_SING_BOX_LOGS"] = "check_sing_box_logs"; AvailableMethods2["CHECK_SING_BOX_LOGS"] = "check_sing_box_logs";
@@ -3333,6 +3336,24 @@ var TachyonShellMethods = {
"/usr/bin/tachyon", "/usr/bin/tachyon",
{ timeout: 3e4 } { 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, [ showSingBoxConfig: async (masked = true) => callBaseMethod(Tachyon.AvailableMethods.SHOW_SING_BOX_CONFIG, [
masked ? "masked" : "raw" masked ? "masked" : "raw"
]), ]),
@@ -5053,6 +5074,9 @@ var initialDiagnosticStore = {
doctor: { doctor: {
loading: false loading: false
}, },
aiDoctor: {
loading: false
},
viewLogs: { viewLogs: {
loading: false loading: false
}, },
@@ -9693,6 +9717,7 @@ function renderAvailableActions({
disable, disable,
globalCheck, globalCheck,
doctor, doctor,
aiDoctor,
viewLogs, viewLogs,
showSingBoxConfig, showSingBoxConfig,
generateBugReport generateBugReport
@@ -9767,6 +9792,16 @@ function renderAvailableActions({
disabled: doctor.disabled 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, [ ...insertIf(viewLogs.visible, [
renderButton({ renderButton({
onClick: viewLogs.onClick, onClick: viewLogs.onClick,
@@ -11088,6 +11123,117 @@ async function handleRunDoctor() {
runChecks(); 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() { async function handleViewLogs() {
setDiagnosticActionLoading("viewLogs", true); setDiagnosticActionLoading("viewLogs", true);
try { try {
@@ -11290,6 +11436,12 @@ function renderDiagnosticAvailableActionsWidget() {
onClick: handleRunDoctor, onClick: handleRunDoctor,
disabled: utilityActionsDisabled disabled: utilityActionsDisabled
}, },
aiDoctor: {
loading: diagnosticsActions.aiDoctor.loading,
visible: true,
onClick: handleRunAiDoctor,
disabled: utilityActionsDisabled
},
viewLogs: { viewLogs: {
loading: diagnosticsActions.viewLogs.loading, loading: diagnosticsActions.viewLogs.loading,
visible: true, visible: true,
+2
View File
@@ -219,6 +219,8 @@ function command_spec(command) {
global_check: [ "diagnostics/runtime.uc", "global-check", 2 ], global_check: [ "diagnostics/runtime.uc", "global-check", 2 ],
doctor: [ "diagnostics/runtime.uc", "doctor", 1 ], doctor: [ "diagnostics/runtime.uc", "doctor", 1 ],
ai_doctor: [ "diagnostics/runtime.uc", "ai-doctor", 0 ], 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 ], diagnose_json: [ "diagnostics/runtime.uc", "diagnose-json", 0 ],
agent_cgi: [ "service/agent_api.uc", "__cgi", 0 ], agent_cgi: [ "service/agent_api.uc", "__cgi", 0 ],
watchdog: [ "service/watchdog.uc", "", 1 ], watchdog: [ "service/watchdog.uc", "", 1 ],
+178 -11
View File
@@ -2941,6 +2941,113 @@ function diagnose_json() {
return 0; 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() { function ai_doctor() {
let cfg = uci_settings(); let cfg = uci_settings();
if (cfg.enable_ai_doctor != "1" || !cfg.ai_doctor_api_key) { 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_out = trim(command_output("cat /proc/uptime 2>/dev/null"));
let uptime_min = uptime_out != "" ? int(split(uptime_out, ".")[0]) / 60 : 0; 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( let sys_context = sprintf(
"Tachyon Doctor Report:\n%s\n" + "Tachyon Doctor Report:\n%s\n\n" +
"System Parameters:\n" +
"Version: %s\n" + "Version: %s\n" +
"DNS type: %s\n" + "DNS type: %s\n" +
"sing-box running: %s\n" + "sing-box running: %s\n" +
"Uptime: %d minutes\n", "Uptime: %d minutes\n\n" +
report, version, dns_type, singbox_running ? "yes" : "no", uptime_min "%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 provider = cfg.ai_doctor_provider || "openai";
let api_key = cfg.ai_doctor_api_key || ""; let api_key = cfg.ai_doctor_api_key || "";
@@ -2984,23 +3134,36 @@ function ai_doctor() {
return 0; return 0;
} }
let quick_fix = ""; let quick_fixes = [];
let text = ai_res; let text = ai_res;
let fix_idx = index(text, "FIX:"); let fix_idx = index(text, "FIX:");
if (fix_idx >= 0) { if (fix_idx >= 0) {
let fix_part = trim(substr(text, fix_idx + 4)); let fix_part = trim(substr(text, fix_idx + 4));
let fields = split(fix_part, /[ \t\r\n]+/); let raw_codes = split(fix_part, /[,\r\n]+/);
if (length(fields) > 0) { for (let code in raw_codes) {
quick_fix = replace(fields[0], /[._`*]/g, ""); 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)); 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, success: true,
timestamp: time(),
report: text, 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; return 0;
} }
@@ -3185,6 +3348,10 @@ else if (mode == "diagnose-json")
exit(diagnose_json()); exit(diagnose_json());
else if (mode == "ai-doctor") else if (mode == "ai-doctor")
exit(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") else if (mode == "validate-nfqws-strategy-json")
exit(validate_nfqws_strategy_json(ARGV[1] || "")); exit(validate_nfqws_strategy_json(ARGV[1] || ""));
else if (mode == "validate-nfqws2-strategy-json") else if (mode == "validate-nfqws2-strategy-json")
@@ -446,6 +446,39 @@ function handle_domain_add(body) {
ok({ message: "Domain '" + domain + "' added to section '" + section + "'" }); 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 ─────────────────────────────────────────────────────────────── // ─── Dispatcher ───────────────────────────────────────────────────────────────
let path_arg = as_string(ARGV[0] || ""); let path_arg = as_string(ARGV[0] || "");
@@ -469,6 +502,8 @@ if (method == "GET") {
handle_config(); handle_config();
else if (route == "/tools" || route == "/tools/") else if (route == "/tools" || route == "/tools/")
handle_tools(); handle_tools();
else if (route == "/ai-doctor/last" || route == "/ai-doctor/last/")
handle_ai_doctor_last();
else else
err("Unknown endpoint: GET " + route, 404); err("Unknown endpoint: GET " + route, 404);
} else if (method == "POST") { } else if (method == "POST") {
@@ -487,6 +522,8 @@ if (method == "GET") {
handle_section_toggle(body); handle_section_toggle(body);
else if (route == "/domain/add") else if (route == "/domain/add")
handle_domain_add(body); handle_domain_add(body);
else if (route == "/ai-doctor/fix" || route == "/ai-doctor/fix/")
handle_ai_doctor_fix(body);
else else
err("Unknown endpoint: POST " + route, 404); err("Unknown endpoint: POST " + route, 404);
} }