Fix HTML display: use plain text + _text_to_html for all bot messages
All bot messages now built as plain text and converted via _text_to_html() which escapes content and converts newlines to <br>. This avoids raw HTML tags appearing literally in Odoo 18 Discuss. - _describe_zip: returns plain str (no Markup/HTML) - _post_file_clarification: builds plain text, posts via _text_to_html() - _find_pending_attachments: strip HTML before phrase matching - _text_to_html: new helper shared by clarification and agent replies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,30 +33,33 @@ def _ext(filename: str) -> str:
|
||||
return filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''
|
||||
|
||||
|
||||
def _describe_zip(datas_b64: str, zip_name: str) -> Markup:
|
||||
"""Return a safe HTML summary of a ZIP's contents without extracting data."""
|
||||
def _text_to_html(text: str) -> Markup:
|
||||
"""Convert plain text to HTML — escapes content, turns newlines into <br>."""
|
||||
return Markup('<br>').join(Markup(escape(line)) for line in text.split('\n'))
|
||||
|
||||
|
||||
def _describe_zip(datas_b64: str, zip_name: str) -> str:
|
||||
"""Return a plain-text summary of a ZIP archive's contents."""
|
||||
try:
|
||||
raw = base64.b64decode(datas_b64)
|
||||
with zipfile.ZipFile(io.BytesIO(raw)) as zf:
|
||||
members = [m for m in zf.namelist() if not m.endswith('/')]
|
||||
if not members:
|
||||
return Markup(f'<b>{escape(zip_name)}</b> (empty archive)')
|
||||
return f'{zip_name} (empty archive)'
|
||||
counts: dict[str, int] = {}
|
||||
for m in members:
|
||||
label = _EXT_LABELS.get(_ext(m), 'file')
|
||||
counts[label] = counts.get(label, 0) + 1
|
||||
type_summary = escape(', '.join(f'{n} {t}(s)' for t, n in counts.items()))
|
||||
lines: list[Markup] = [
|
||||
Markup(f'<b>{escape(zip_name)}</b> — {len(members)} item(s): {type_summary}')
|
||||
]
|
||||
type_summary = ', '.join(f'{n} {t}(s)' for t, n in counts.items())
|
||||
lines = [f'{zip_name} -- {len(members)} item(s): {type_summary}']
|
||||
for m in members[:8]:
|
||||
lines.append(Markup(f' • {escape(m)}'))
|
||||
lines.append(f' - {m}')
|
||||
if len(members) > 8:
|
||||
lines.append(Markup(f' … and {len(members) - 8} more'))
|
||||
return Markup('<br>').join(lines)
|
||||
lines.append(f' ... and {len(members) - 8} more')
|
||||
return '\n'.join(lines)
|
||||
except Exception as exc:
|
||||
_logger.warning('_describe_zip failed for %s: %s', zip_name, exc)
|
||||
return Markup(f'<b>{escape(zip_name)}</b> (could not inspect contents)')
|
||||
return f'{zip_name} (could not inspect contents)'
|
||||
|
||||
|
||||
class DiscussChannel(models.Model):
|
||||
@@ -90,7 +93,6 @@ class DiscussChannel(models.Model):
|
||||
# Odoo 18 Discuss uploads attachments before posting the message and
|
||||
# passes their IDs in kwargs as attachment_ids (list of ints or ORM
|
||||
# commands). result.attachment_ids resolves those after super() runs.
|
||||
# Log both so we can see exactly what arrives.
|
||||
_logger.info(
|
||||
'AB AI mail hook: body=%r kwargs_keys=%s '
|
||||
'attachment_ids_kwarg=%r result.attachment_ids=%s',
|
||||
@@ -106,13 +108,13 @@ class DiscussChannel(models.Model):
|
||||
if not text and not attachments:
|
||||
return result
|
||||
|
||||
# ── Case 1: file(s) with no instruction ──────────────────────────────
|
||||
# -- Case 1: file(s) with no instruction --------------------------------
|
||||
# Show the user what we received and ask what to do with it.
|
||||
if attachments and not text:
|
||||
self._post_file_clarification(attachments, bot_partner)
|
||||
return result
|
||||
|
||||
# ── Case 2: text only — look back for a pending file upload ──────────
|
||||
# -- Case 2: text only -- look back for a pending file upload -----------
|
||||
# If the user just replied to our clarification question, find the
|
||||
# attachment(s) they uploaded earlier in this conversation.
|
||||
pending = self.env['ir.attachment'].browse()
|
||||
@@ -121,7 +123,7 @@ class DiscussChannel(models.Model):
|
||||
|
||||
effective_attachments = attachments or pending
|
||||
|
||||
# ── Case 3: text (+ possibly pending files) → dispatch ───────────────
|
||||
# -- Case 3: text (+ possibly pending files) -> dispatch ----------------
|
||||
human_partner = member_partners.filtered(lambda p: p != bot_partner)[:1]
|
||||
user = self.env['res.users'].search([('partner_id', '=', human_partner.id)], limit=1)
|
||||
uid = user.id if user else self.env.uid
|
||||
@@ -147,10 +149,8 @@ class DiscussChannel(models.Model):
|
||||
|
||||
reply_text = ((response or {}).get('reply') or (response or {}).get('message') or
|
||||
'I could not process your request right now.')
|
||||
# Agent replies are plain text; convert newlines to <br> for Discuss.
|
||||
reply_html = Markup('<br>').join(Markup(escape(ln)) for ln in reply_text.split('\n'))
|
||||
self.sudo().message_post(
|
||||
body=reply_html,
|
||||
body=_text_to_html(reply_text),
|
||||
author_id=bot_partner.id,
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
@@ -162,28 +162,28 @@ class DiscussChannel(models.Model):
|
||||
|
||||
def _post_file_clarification(self, attachments, bot_partner):
|
||||
"""Describe the uploaded file(s) and ask the user what to do with them."""
|
||||
lines: list[Markup] = []
|
||||
file_lines = []
|
||||
for att in attachments:
|
||||
name = att.name or 'file'
|
||||
ext = _ext(name)
|
||||
if ext == 'zip' and att.datas:
|
||||
lines.append(_describe_zip(att.datas, name))
|
||||
file_lines.append(_describe_zip(att.datas, name))
|
||||
else:
|
||||
label = _EXT_LABELS.get(ext, 'file')
|
||||
lines.append(Markup(f'<b>{escape(name)}</b> ({escape(label)})'))
|
||||
file_lines.append(f'{name} ({label})')
|
||||
|
||||
file_summary = Markup('<br>').join(lines)
|
||||
question = Markup(
|
||||
'I received the following file(s):<br>'
|
||||
) + file_summary + Markup(
|
||||
'<br><br>'
|
||||
'What would you like me to do with them? Some options:<br>'
|
||||
'• <b>Create an expense report</b> from these receipts<br>'
|
||||
'• <b>Import products</b> from this data<br>'
|
||||
'• <b>Something else</b> — just tell me what you need'
|
||||
file_summary = '\n'.join(file_lines)
|
||||
question = (
|
||||
f'I received the following file(s):\n'
|
||||
f'{file_summary}\n'
|
||||
f'\n'
|
||||
f'What would you like me to do with them? Some options:\n'
|
||||
f' - Create an expense report from these receipts\n'
|
||||
f' - Import products from this data\n'
|
||||
f' - Something else -- just tell me what you need'
|
||||
)
|
||||
self.sudo().message_post(
|
||||
body=question,
|
||||
body=_text_to_html(question),
|
||||
author_id=bot_partner.id,
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
@@ -207,7 +207,7 @@ class DiscussChannel(models.Model):
|
||||
for msg in messages:
|
||||
is_bot = msg.author_id == bot_partner
|
||||
if is_bot:
|
||||
body_lower = (msg.body or '').lower()
|
||||
body_lower = _strip_html(msg.body or '').lower()
|
||||
if any(p in body_lower for p in _bot_question_phrases):
|
||||
prev_was_bot_question = True
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user