mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-05-01 11:56:21 +00:00
Update notification service
This commit is contained in:
@@ -61,6 +61,7 @@ interface NotificationConfig {
|
|||||||
ai_model: string
|
ai_model: string
|
||||||
ai_language: string
|
ai_language: string
|
||||||
ai_ollama_url: string
|
ai_ollama_url: string
|
||||||
|
ai_openai_base_url: string
|
||||||
channel_ai_detail: Record<string, string>
|
channel_ai_detail: Record<string, string>
|
||||||
hostname: string
|
hostname: string
|
||||||
webhook_secret: string
|
webhook_secret: string
|
||||||
@@ -211,6 +212,7 @@ const DEFAULT_CONFIG: NotificationConfig = {
|
|||||||
ai_model: "",
|
ai_model: "",
|
||||||
ai_language: "en",
|
ai_language: "en",
|
||||||
ai_ollama_url: "http://localhost:11434",
|
ai_ollama_url: "http://localhost:11434",
|
||||||
|
ai_openai_base_url: "",
|
||||||
channel_ai_detail: {
|
channel_ai_detail: {
|
||||||
telegram: "brief",
|
telegram: "brief",
|
||||||
gotify: "brief",
|
gotify: "brief",
|
||||||
@@ -1390,6 +1392,26 @@ export function NotificationSettings() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Custom Base URL for OpenAI-compatible APIs */}
|
||||||
|
{config.ai_provider === "openai" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label className="text-xs sm:text-sm text-foreground/80">Custom Base URL</Label>
|
||||||
|
<span className="text-xs text-muted-foreground">(optional)</span>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
className="h-9 text-sm font-mono"
|
||||||
|
placeholder="Leave empty for OpenAI, or enter custom endpoint"
|
||||||
|
value={config.ai_openai_base_url}
|
||||||
|
onChange={e => updateConfig(p => ({ ...p, ai_openai_base_url: e.target.value }))}
|
||||||
|
disabled={!editMode}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
For OpenAI-compatible APIs: BytePlus, LocalAI, LM Studio, vLLM, etc.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* API Key (not shown for Ollama) */}
|
{/* API Key (not shown for Ollama) */}
|
||||||
{config.ai_provider !== "ollama" && (
|
{config.ai_provider !== "ollama" && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -1584,6 +1606,21 @@ export function NotificationSettings() {
|
|||||||
<p className="text-xs sm:text-sm text-muted-foreground mt-2 ml-[52px] leading-relaxed">
|
<p className="text-xs sm:text-sm text-muted-foreground mt-2 ml-[52px] leading-relaxed">
|
||||||
{provider.description}
|
{provider.description}
|
||||||
</p>
|
</p>
|
||||||
|
{/* OpenAI compatibility note */}
|
||||||
|
{provider.value === "openai" && (
|
||||||
|
<div className="mt-3 ml-[52px] p-3 rounded-md bg-blue-500/10 border border-blue-500/20">
|
||||||
|
<p className="text-xs sm:text-sm text-blue-400 font-medium mb-1">OpenAI-Compatible APIs</p>
|
||||||
|
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||||
|
You can use any OpenAI-compatible API by setting a custom Base URL. Compatible services include:
|
||||||
|
</p>
|
||||||
|
<ul className="text-xs text-muted-foreground mt-1.5 space-y-0.5 ml-3">
|
||||||
|
<li>BytePlus/ByteDance (Kimi K2.5)</li>
|
||||||
|
<li>LocalAI, LM Studio, vLLM</li>
|
||||||
|
<li>Together AI, Fireworks AI</li>
|
||||||
|
<li>Any service using OpenAI format</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,16 +8,37 @@ from .base import AIProvider, AIProviderError
|
|||||||
|
|
||||||
|
|
||||||
class OpenAIProvider(AIProvider):
|
class OpenAIProvider(AIProvider):
|
||||||
"""OpenAI provider using their Chat Completions API."""
|
"""OpenAI provider using their Chat Completions API.
|
||||||
|
|
||||||
|
Also compatible with OpenAI-compatible APIs like:
|
||||||
|
- BytePlus/ByteDance (Kimi K2.5)
|
||||||
|
- LocalAI
|
||||||
|
- LM Studio
|
||||||
|
- vLLM
|
||||||
|
- Together AI
|
||||||
|
- Any OpenAI-compatible endpoint
|
||||||
|
"""
|
||||||
|
|
||||||
NAME = "openai"
|
NAME = "openai"
|
||||||
DEFAULT_MODEL = "gpt-4o-mini"
|
DEFAULT_MODEL = "gpt-4o-mini"
|
||||||
REQUIRES_API_KEY = True
|
REQUIRES_API_KEY = True
|
||||||
API_URL = "https://api.openai.com/v1/chat/completions"
|
DEFAULT_API_URL = "https://api.openai.com/v1/chat/completions"
|
||||||
|
|
||||||
|
def _get_api_url(self) -> str:
|
||||||
|
"""Get the API URL, using custom base_url if provided."""
|
||||||
|
if self.base_url:
|
||||||
|
# Ensure the URL ends with the correct path
|
||||||
|
base = self.base_url.rstrip('/')
|
||||||
|
if not base.endswith('/chat/completions'):
|
||||||
|
if not base.endswith('/v1'):
|
||||||
|
base = f"{base}/v1"
|
||||||
|
base = f"{base}/chat/completions"
|
||||||
|
return base
|
||||||
|
return self.DEFAULT_API_URL
|
||||||
|
|
||||||
def generate(self, system_prompt: str, user_message: str,
|
def generate(self, system_prompt: str, user_message: str,
|
||||||
max_tokens: int = 200) -> Optional[str]:
|
max_tokens: int = 200) -> Optional[str]:
|
||||||
"""Generate a response using OpenAI's API.
|
"""Generate a response using OpenAI's API or compatible endpoint.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
system_prompt: System instructions
|
system_prompt: System instructions
|
||||||
@@ -48,7 +69,8 @@ class OpenAIProvider(AIProvider):
|
|||||||
'Authorization': f'Bearer {self.api_key}',
|
'Authorization': f'Bearer {self.api_key}',
|
||||||
}
|
}
|
||||||
|
|
||||||
result = self._make_request(self.API_URL, payload, headers)
|
api_url = self._get_api_url()
|
||||||
|
result = self._make_request(api_url, payload, headers)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return result['choices'][0]['message']['content'].strip()
|
return result['choices'][0]['message']['content'].strip()
|
||||||
|
|||||||
@@ -1162,9 +1162,11 @@ class NotificationManager:
|
|||||||
ai_status = f'AI: enabled ({ai_provider} / {ai_language})' if ai_enabled else 'AI: disabled'
|
ai_status = f'AI: enabled ({ai_provider} / {ai_language})' if ai_enabled else 'AI: disabled'
|
||||||
|
|
||||||
# Base test message — shows current channel config
|
# Base test message — shows current channel config
|
||||||
|
# NOTE: narrative lines are intentionally unlabeled so the AI
|
||||||
|
# does not prepend "Message:" or other spurious field labels.
|
||||||
base_title = 'ProxMenux Test'
|
base_title = 'ProxMenux Test'
|
||||||
base_message = (
|
base_message = (
|
||||||
'Welcome to ProxMenux Monitor!\n'
|
'Welcome to ProxMenux Monitor!\n\n'
|
||||||
'This is a test message to verify your notification channel is working correctly.\n\n'
|
'This is a test message to verify your notification channel is working correctly.\n\n'
|
||||||
'Channel configuration:\n'
|
'Channel configuration:\n'
|
||||||
f'{icon_status}\n'
|
f'{icon_status}\n'
|
||||||
|
|||||||
@@ -1064,9 +1064,9 @@ EVENT_EMOJI = {
|
|||||||
'replication_fail': '\u274C',
|
'replication_fail': '\u274C',
|
||||||
'replication_complete': '\u2705',
|
'replication_complete': '\u2705',
|
||||||
# Backups
|
# Backups
|
||||||
'backup_start': '\U0001F4E6', # package
|
'backup_start': '\U0001F4BE\U0001F680', # 💾🚀 floppy + rocket
|
||||||
'backup_complete': '\u2705',
|
'backup_complete': '\U0001F4BE\u2705', # 💾✅ floppy + check
|
||||||
'backup_fail': '\u274C',
|
'backup_fail': '\U0001F4BE\u274C', # 💾❌ floppy + cross
|
||||||
'snapshot_complete': '\U0001F4F8', # camera with flash
|
'snapshot_complete': '\U0001F4F8', # camera with flash
|
||||||
'snapshot_fail': '\u274C',
|
'snapshot_fail': '\u274C',
|
||||||
# Resources
|
# Resources
|
||||||
@@ -1240,7 +1240,7 @@ AI_LANGUAGES = {
|
|||||||
AI_DETAIL_TOKENS = {
|
AI_DETAIL_TOKENS = {
|
||||||
'brief': 100, # 2-3 lines, essential only
|
'brief': 100, # 2-3 lines, essential only
|
||||||
'standard': 200, # Concise paragraph with context
|
'standard': 200, # Concise paragraph with context
|
||||||
'detailed': 400, # Complete technical details
|
'detailed': 700, # Complete technical details (raised: multi-VM backups can be long)
|
||||||
}
|
}
|
||||||
|
|
||||||
# System prompt template - informative, no recommendations
|
# System prompt template - informative, no recommendations
|
||||||
@@ -1255,7 +1255,9 @@ Your task is to translate and reformat incoming server alert messages into {lang
|
|||||||
4. Tone: factual, concise, technical. No greetings, no closings, no apologies
|
4. Tone: factual, concise, technical. No greetings, no closings, no apologies
|
||||||
5. DO NOT add recommendations, action items, or suggestions ("you should…", "consider…")
|
5. DO NOT add recommendations, action items, or suggestions ("you should…", "consider…")
|
||||||
6. Present ONLY the facts already in the input — do not invent or assume information
|
6. Present ONLY the facts already in the input — do not invent or assume information
|
||||||
7. Detail level to apply: {detail_level}
|
7. PLAIN NARRATIVE LINES — if a line in the input is a complete sentence (not a "Label: value"
|
||||||
|
pair), translate it as-is. Never prepend "Message:", "Note:", or any other label to a sentence.
|
||||||
|
8. Detail level to apply: {detail_level}
|
||||||
- brief → 2-3 lines, essential data only (status + key metric)
|
- brief → 2-3 lines, essential data only (status + key metric)
|
||||||
- standard → short paragraph covering who/what/where and the key value
|
- standard → short paragraph covering who/what/where and the key value
|
||||||
- detailed → full technical breakdown of all available fields
|
- detailed → full technical breakdown of all available fields
|
||||||
@@ -1271,13 +1273,22 @@ Your task is to translate and reformat incoming server alert messages into {lang
|
|||||||
|
|
||||||
BACKUP (backup_complete / backup_fail / backup_start):
|
BACKUP (backup_complete / backup_fail / backup_start):
|
||||||
Input contains: VM/CT names, IDs, size, duration, storage location, status per VM
|
Input contains: VM/CT names, IDs, size, duration, storage location, status per VM
|
||||||
Output body must list each VM on its own line: name, ID, status (ok/error), size, duration
|
Output body: first line is plain text (no emoji) describing the event briefly.
|
||||||
End with a summary line: total VMs, total size, total time
|
Then list each VM/CT with its fields. End with a summary line.
|
||||||
|
PARTIAL FAILURE RULE: if some VMs succeeded and at least one failed, use a combined title
|
||||||
|
like "Backup partially failed" / "Copia de seguridad parcialmente fallida" — never say
|
||||||
|
"backup failed" when there are also successful VMs in the same job.
|
||||||
|
NEVER omit the storage/archive line or the summary line — always include them even for long jobs.
|
||||||
|
|
||||||
UPDATES (update_summary / pve_update):
|
UPDATES (update_summary / pve_update):
|
||||||
Input contains: total count, security count, proxmox count, kernel count, package list
|
Input contains: total count, security count, proxmox count, kernel count, package list
|
||||||
Output body must show each count on its own line with its label
|
Output body must show each count on its own line with its label.
|
||||||
List important packages below, one per line
|
For the package list: use "• " (bullet + space) before each package name, NOT the 📋 emoji.
|
||||||
|
The 📋 emoji goes only on the "Important packages:" header line.
|
||||||
|
Example packages block:
|
||||||
|
📋 Important packages:
|
||||||
|
• pve-manager (9.1.4 -> 9.1.6)
|
||||||
|
• qemu-server (9.1.3 -> 9.1.4)
|
||||||
|
|
||||||
DISK / SMART ERRORS (disk_io_error / storage_unavailable):
|
DISK / SMART ERRORS (disk_io_error / storage_unavailable):
|
||||||
Input contains: device name, error type, SMART values or I/O error codes
|
Input contains: device name, error type, SMART values or I/O error codes
|
||||||
@@ -1403,37 +1414,61 @@ AI_EMOJI_INSTRUCTIONS = """
|
|||||||
⚙️ Kernel updates: 0
|
⚙️ Kernel updates: 0
|
||||||
|
|
||||||
📋 Important packages:
|
📋 Important packages:
|
||||||
📋 none
|
• none
|
||||||
|
|
||||||
EXAMPLE — updates message (with important packages):
|
EXAMPLE — updates message (with important packages):
|
||||||
[TITLE]
|
[TITLE]
|
||||||
📦 amd: Updates available
|
📦 amd: Updates available
|
||||||
[BODY]
|
[BODY]
|
||||||
📦 Total updates: 55
|
📦 Total updates: 90
|
||||||
🔒 Security updates: 3
|
🔒 Security updates: 6
|
||||||
🔄 Proxmox updates: 2
|
🔄 Proxmox updates: 14
|
||||||
⚙️ Kernel updates: 1
|
⚙️ Kernel updates: 1
|
||||||
|
|
||||||
📋 Important packages:
|
📋 Important packages:
|
||||||
📋 pve-manager
|
• pve-manager (9.1.4 -> 9.1.6)
|
||||||
📋 libssl3
|
• qemu-server (9.1.3 -> 9.1.4)
|
||||||
|
• pve-container (6.0.18 -> 6.1.2)
|
||||||
|
|
||||||
EXAMPLE — backup complete with multiple VMs:
|
EXAMPLE — backup complete with multiple VMs:
|
||||||
[TITLE]
|
[TITLE]
|
||||||
✅ pve01: Backup complete
|
💾✅ pve01: Backup complete
|
||||||
[BODY]
|
[BODY]
|
||||||
|
Backup job finished on storage local-bak.
|
||||||
|
|
||||||
🏷️ VM web01 (ID: 100)
|
🏷️ VM web01 (ID: 100)
|
||||||
✅ Status: ok
|
✅ Status: ok
|
||||||
📏 Size: 12.3 GiB
|
📏 Size: 12.3 GiB
|
||||||
⏱️ Duration: 00:04:21
|
⏱️ Duration: 00:04:21
|
||||||
|
🗄️ Storage: vm/100/2026-03-17T22:00:08Z
|
||||||
|
|
||||||
🏷️ CT db (ID: 101)
|
🏷️ CT db (ID: 101)
|
||||||
✅ Status: ok
|
✅ Status: ok
|
||||||
📏 Size: 4.1 GiB
|
📏 Size: 4.1 GiB
|
||||||
⏱️ Duration: 00:01:10
|
⏱️ Duration: 00:01:10
|
||||||
|
🗄️ Storage: ct/101/2026-03-17T22:04:29Z
|
||||||
|
|
||||||
📊 Total: 2 backups | 16.4 GiB | ⏱️ 00:05:31
|
📊 Total: 2 backups | 16.4 GiB | ⏱️ 00:05:31
|
||||||
|
|
||||||
|
EXAMPLE — backup partially failed (some ok, some failed):
|
||||||
|
[TITLE]
|
||||||
|
💾❌ pve01: Backup partially failed
|
||||||
|
[BODY]
|
||||||
|
Backup job finished with errors on storage PBS2.
|
||||||
|
|
||||||
|
🏷️ VM web01 (ID: 100)
|
||||||
|
✅ Status: ok
|
||||||
|
📏 Size: 12.3 GiB
|
||||||
|
⏱️ Duration: 00:04:21
|
||||||
|
🗄️ Storage: vm/100/2026-03-17T22:00:08Z
|
||||||
|
|
||||||
|
🏷️ VM broken (ID: 102)
|
||||||
|
❌ Status: error
|
||||||
|
📏 Size: 0 B
|
||||||
|
⏱️ Duration: 00:00:37
|
||||||
|
|
||||||
|
📊 Total: 2 backups | ❌ 1 failed | 12.3 GiB | ⏱️ 00:04:58
|
||||||
|
|
||||||
EXAMPLE — disk I/O health warning:
|
EXAMPLE — disk I/O health warning:
|
||||||
[TITLE]
|
[TITLE]
|
||||||
💥 amd: Health warning — Disk I/O errors
|
💥 amd: Health warning — Disk I/O errors
|
||||||
@@ -1493,11 +1528,20 @@ class AIEnhancer:
|
|||||||
from ai_providers import get_provider
|
from ai_providers import get_provider
|
||||||
|
|
||||||
provider_name = self.config.get('ai_provider', 'groq')
|
provider_name = self.config.get('ai_provider', 'groq')
|
||||||
|
|
||||||
|
# Determine base_url based on provider
|
||||||
|
if provider_name == 'ollama':
|
||||||
|
base_url = self.config.get('ai_ollama_url', '')
|
||||||
|
elif provider_name == 'openai':
|
||||||
|
base_url = self.config.get('ai_openai_base_url', '')
|
||||||
|
else:
|
||||||
|
base_url = ''
|
||||||
|
|
||||||
self._provider = get_provider(
|
self._provider = get_provider(
|
||||||
provider_name,
|
provider_name,
|
||||||
api_key=self.config.get('ai_api_key', ''),
|
api_key=self.config.get('ai_api_key', ''),
|
||||||
model=self.config.get('ai_model', ''),
|
model=self.config.get('ai_model', ''),
|
||||||
base_url=self.config.get('ai_ollama_url', ''),
|
base_url=base_url,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[AIEnhancer] Failed to initialize provider: {e}")
|
print(f"[AIEnhancer] Failed to initialize provider: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user