mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-07 14:06:40 +00:00
Preserve vzdump inventory when AI is enabled
This commit is contained in:
@@ -308,18 +308,51 @@ class TelegramChannel(NotificationChannel):
|
|||||||
return self._http_request(url, payload, {'Content-Type': 'application/json'})
|
return self._http_request(url, payload, {'Content-Type': 'application/json'})
|
||||||
|
|
||||||
def _split_message(self, text: str) -> list:
|
def _split_message(self, text: str) -> list:
|
||||||
|
"""Split Telegram HTML without cutting entities or formatting tags.
|
||||||
|
|
||||||
|
Open formatting tags are closed at the end of a chunk and reopened in
|
||||||
|
the next one, so every API request is valid HTML on its own.
|
||||||
|
"""
|
||||||
if len(text) <= self.MAX_LENGTH:
|
if len(text) <= self.MAX_LENGTH:
|
||||||
return [text]
|
return [text]
|
||||||
|
|
||||||
|
token_re = re.compile(
|
||||||
|
r'&(?:#[0-9]+|#x[0-9A-Fa-f]+|[A-Za-z][A-Za-z0-9]+);|<[^<>]+>|.',
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
tag_re = re.compile(r'<\s*(/?)\s*([A-Za-z0-9-]+)(?:\s[^<>]*)?>')
|
||||||
|
void_tags = {'br'}
|
||||||
|
|
||||||
|
def _advance(stack, token):
|
||||||
|
match = tag_re.fullmatch(token)
|
||||||
|
if not match:
|
||||||
|
return list(stack)
|
||||||
|
closing, name = match.groups()
|
||||||
|
name = name.lower()
|
||||||
|
next_stack = list(stack)
|
||||||
|
if closing:
|
||||||
|
if next_stack and next_stack[-1][0] == name:
|
||||||
|
next_stack.pop()
|
||||||
|
elif not token.rstrip().endswith('/>') and name not in void_tags:
|
||||||
|
next_stack.append((name, token))
|
||||||
|
return next_stack
|
||||||
|
|
||||||
|
def _closers(stack):
|
||||||
|
return ''.join(f'</{name}>' for name, _ in reversed(stack))
|
||||||
|
|
||||||
chunks = []
|
chunks = []
|
||||||
while text:
|
current = ''
|
||||||
if len(text) <= self.MAX_LENGTH:
|
open_tags = []
|
||||||
chunks.append(text)
|
for token in token_re.findall(text):
|
||||||
break
|
next_tags = _advance(open_tags, token)
|
||||||
split_at = text.rfind('\n', 0, self.MAX_LENGTH)
|
if current and len(current) + len(token) + len(_closers(next_tags)) > self.MAX_LENGTH:
|
||||||
if split_at == -1:
|
chunks.append(current + _closers(open_tags))
|
||||||
split_at = self.MAX_LENGTH
|
current = ''.join(opener for _, opener in open_tags)
|
||||||
chunks.append(text[:split_at])
|
current += token
|
||||||
text = text[split_at:].lstrip('\n')
|
open_tags = _advance(open_tags, token)
|
||||||
|
|
||||||
|
if current:
|
||||||
|
chunks.append(current + _closers(open_tags))
|
||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -949,6 +982,16 @@ class EmailChannel(NotificationChannel):
|
|||||||
# ── Build structured detail rows from known data fields ──
|
# ── Build structured detail rows from known data fields ──
|
||||||
detail_rows = self._build_detail_rows(data, event_type, group, html_mod)
|
detail_rows = self._build_detail_rows(data, event_type, group, html_mod)
|
||||||
|
|
||||||
|
# Vzdump bodies are authoritative multi-item inventories. Structured
|
||||||
|
# backup metadata is only a summary and previously replaced the body in
|
||||||
|
# the HTML alternative, hiding all guest rows. Render every body line
|
||||||
|
# exactly once for these events instead of mixing both representations.
|
||||||
|
if event_type in {'backup_complete', 'backup_fail'}:
|
||||||
|
detail_rows = [
|
||||||
|
('', html_mod.escape(line.strip()))
|
||||||
|
for line in body.split('\n') if line.strip()
|
||||||
|
]
|
||||||
|
|
||||||
# ── Fallback: if no structured rows, render body text lines ──
|
# ── Fallback: if no structured rows, render body text lines ──
|
||||||
if not detail_rows:
|
if not detail_rows:
|
||||||
for line in body.split('\n'):
|
for line in body.split('\n'):
|
||||||
|
|||||||
@@ -1376,27 +1376,32 @@ class NotificationManager:
|
|||||||
# raw template-formatted notification. Audit Tier 6 —
|
# raw template-formatted notification. Audit Tier 6 —
|
||||||
# `_dispatch_to_channels`: AI failure dropped the notification.
|
# `_dispatch_to_channels`: AI failure dropped the notification.
|
||||||
try:
|
try:
|
||||||
enriched_context = enrich_context_for_ai(
|
# Backup reports are authoritative inventories. A model can
|
||||||
title=ch_title,
|
# neither be trusted to avoid repeating all guest rows nor to
|
||||||
body=ch_body,
|
# preserve every value, so these two events bypass AI entirely.
|
||||||
event_type=event_type,
|
ai_result = None
|
||||||
data=data,
|
if event_type not in {'backup_complete', 'backup_fail'}:
|
||||||
journal_context=raw_journal_context,
|
enriched_context = enrich_context_for_ai(
|
||||||
detail_level=detail_level
|
title=ch_title,
|
||||||
)
|
body=ch_body,
|
||||||
|
event_type=event_type,
|
||||||
|
data=data,
|
||||||
|
journal_context=raw_journal_context,
|
||||||
|
detail_level=detail_level
|
||||||
|
)
|
||||||
|
|
||||||
# Wrap the AI rewrite with a hard timeout so a slow Ollama
|
# Wrap the AI rewrite with a hard timeout so a slow Ollama
|
||||||
# call (90-120 s on slow CPUs) doesn't stall the dispatch
|
# call (90-120 s on slow CPUs) doesn't stall the dispatch
|
||||||
# thread and delay every other queued event. On timeout we
|
# thread and delay every other queued event. On timeout we
|
||||||
# ship the non-AI title/body — the user still gets the
|
# ship the non-AI title/body — the user still gets the
|
||||||
# notification, just without LLM polish. Audit Tier 3.2 #2.
|
# notification, just without LLM polish. Audit Tier 3.2 #2.
|
||||||
ai_result = _format_with_ai_bounded(
|
ai_result = _format_with_ai_bounded(
|
||||||
format_with_ai_full,
|
format_with_ai_full,
|
||||||
ch_title, ch_body, severity, channel_ai_config,
|
ch_title, ch_body, severity, channel_ai_config,
|
||||||
detail_level=detail_level,
|
detail_level=detail_level,
|
||||||
journal_context=enriched_context,
|
journal_context=enriched_context,
|
||||||
use_emojis=use_rich_format,
|
use_emojis=use_rich_format,
|
||||||
)
|
)
|
||||||
if ai_result is not None:
|
if ai_result is not None:
|
||||||
ch_title = ai_result.get('title', ch_title)
|
ch_title = ai_result.get('title', ch_title)
|
||||||
ch_body = ai_result.get('body', ch_body)
|
ch_body = ai_result.get('body', ch_body)
|
||||||
|
|||||||
Reference in New Issue
Block a user