diff --git a/AGENT.md b/AGENT.md index 92c125ef..14aaa50b 100644 --- a/AGENT.md +++ b/AGENT.md @@ -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:///cgi-bin/tachyon-agent/ ### 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 - - 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 - ``` \ No newline at end of file +- **По умолчанию WAN-доступ к HTTP API закрыт** брандмауэром OpenWrt. \ No newline at end of file diff --git a/fe-app-tachyon/src/tachyon/methods/shell/index.ts b/fe-app-tachyon/src/tachyon/methods/shell/index.ts index 164d8a11..b61d4982 100644 --- a/fe-app-tachyon/src/tachyon/methods/shell/index.ts +++ b/fe-app-tachyon/src/tachyon/methods/shell/index.ts @@ -334,6 +334,27 @@ export const TachyonShellMethods = { '/usr/bin/tachyon', { timeout: 30000 }, ), + aiDoctor: async () => + callBaseMethod( + Tachyon.AvailableMethods.AI_DOCTOR, + [], + '/usr/bin/tachyon', + { timeout: 60000 }, + ), + aiDoctorLast: async () => + callBaseMethod( + Tachyon.AvailableMethods.AI_DOCTOR_LAST, + [], + '/usr/bin/tachyon', + { timeout: 10000 }, + ), + applyQuickFix: async (fixCode: string) => + callBaseMethod( + Tachyon.AvailableMethods.APPLY_QUICK_FIX, + [fixCode], + '/usr/bin/tachyon', + { timeout: 30000 }, + ), showSingBoxConfig: async (masked = true) => callBaseMethod(Tachyon.AvailableMethods.SHOW_SING_BOX_CONFIG, [ masked ? 'masked' : 'raw', diff --git a/fe-app-tachyon/src/tachyon/services/store.service.ts b/fe-app-tachyon/src/tachyon/services/store.service.ts index 79c77050..8c508cc9 100644 --- a/fe-app-tachyon/src/tachyon/services/store.service.ts +++ b/fe-app-tachyon/src/tachyon/services/store.service.ts @@ -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 }; diff --git a/fe-app-tachyon/src/tachyon/tabs/diagnostic/diagnostic.store.ts b/fe-app-tachyon/src/tachyon/tabs/diagnostic/diagnostic.store.ts index 7a809623..6357f8d2 100644 --- a/fe-app-tachyon/src/tachyon/tabs/diagnostic/diagnostic.store.ts +++ b/fe-app-tachyon/src/tachyon/tabs/diagnostic/diagnostic.store.ts @@ -120,6 +120,9 @@ export const initialDiagnosticStore: Pick< doctor: { loading: false, }, + aiDoctor: { + loading: false, + }, viewLogs: { loading: false, }, diff --git a/fe-app-tachyon/src/tachyon/tabs/diagnostic/initController.ts b/fe-app-tachyon/src/tachyon/tabs/diagnostic/initController.ts index 37b3cb73..bf3ff3ab 100644 --- a/fe-app-tachyon/src/tachyon/tabs/diagnostic/initController.ts +++ b/fe-app-tachyon/src/tachyon/tabs/diagnostic/initController.ts @@ -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) + : 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, diff --git a/fe-app-tachyon/src/tachyon/tabs/diagnostic/partials/renderAvailableActions.ts b/fe-app-tachyon/src/tachyon/tabs/diagnostic/partials/renderAvailableActions.ts index dca4e877..e43281f4 100644 --- a/fe-app-tachyon/src/tachyon/tabs/diagnostic/partials/renderAvailableActions.ts +++ b/fe-app-tachyon/src/tachyon/tabs/diagnostic/partials/renderAvailableActions.ts @@ -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, diff --git a/fe-app-tachyon/src/tachyon/types.ts b/fe-app-tachyon/src/tachyon/types.ts index c1194ea4..5a7d5847 100644 --- a/fe-app-tachyon/src/tachyon/types.ts +++ b/fe-app-tachyon/src/tachyon/types.ts @@ -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', diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js index c617fe63..7a12fa4a 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js @@ -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, diff --git a/tachyon/files/usr/bin/tachyon b/tachyon/files/usr/bin/tachyon index 61630e51..14b8ec7b 100755 --- a/tachyon/files/usr/bin/tachyon +++ b/tachyon/files/usr/bin/tachyon @@ -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 ], diff --git a/tachyon/files/usr/lib/diagnostics/runtime.uc b/tachyon/files/usr/lib/diagnostics/runtime.uc index d7e60cdf..b245990c 100644 --- a/tachyon/files/usr/lib/diagnostics/runtime.uc +++ b/tachyon/files/usr/lib/diagnostics/runtime.uc @@ -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") diff --git a/tachyon/files/usr/lib/service/agent_api.uc b/tachyon/files/usr/lib/service/agent_api.uc index 61c90f8a..e18e5a5f 100644 --- a/tachyon/files/usr/lib/service/agent_api.uc +++ b/tachyon/files/usr/lib/service/agent_api.uc @@ -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); }