feat(CF-1812): Use confluence-collab for section-based page editing
Replace inline regex section parser in voice.py with confluence_collab library (BS4 parsing, 409 conflict retry). Bot now loads section outline into LLM context when Confluence links are detected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
68
voice.py
68
voice.py
@@ -279,67 +279,21 @@ async def _confluence_read_page(page_id: str) -> tuple[str, str, int]:
|
||||
async def _confluence_update_section(page_id: str, section_heading: str, new_html: str) -> str:
|
||||
"""Update a section of a Confluence page by heading.
|
||||
|
||||
Finds the section by heading, replaces content up to next same-level heading,
|
||||
PUTs with incremented version.
|
||||
Uses confluence_collab library for BS4 parsing and 409 conflict retry.
|
||||
"""
|
||||
if not CONFLUENCE_URL or not CONFLUENCE_USER or not CONFLUENCE_TOKEN:
|
||||
return "Confluence credentials not configured."
|
||||
# Read current page
|
||||
url = f"{CONFLUENCE_URL}/rest/api/content/{page_id}"
|
||||
params = {"expand": "body.storage,version,title"}
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.get(url, params=params, auth=(CONFLUENCE_USER, CONFLUENCE_TOKEN))
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
from confluence_collab.client import Auth
|
||||
from confluence_collab.editor import section_update
|
||||
|
||||
title = data["title"]
|
||||
version = data["version"]["number"]
|
||||
body_html = data["body"]["storage"]["value"]
|
||||
|
||||
# Find section by heading (h1-h6) and replace content up to next same-level heading
|
||||
heading_pattern = re.compile(
|
||||
r'(<h([1-6])[^>]*>.*?' + re.escape(section_heading) + r'.*?</h\2>)',
|
||||
re.IGNORECASE | re.DOTALL,
|
||||
)
|
||||
match = heading_pattern.search(body_html)
|
||||
if not match:
|
||||
return f"Section '{section_heading}' not found on page."
|
||||
|
||||
heading_tag = match.group(0)
|
||||
heading_level = match.group(2)
|
||||
section_start = match.end()
|
||||
|
||||
# Find next heading of same or higher level
|
||||
next_heading = re.compile(
|
||||
rf'<h[1-{heading_level}][^>]*>',
|
||||
re.IGNORECASE,
|
||||
)
|
||||
next_match = next_heading.search(body_html, section_start)
|
||||
section_end = next_match.start() if next_match else len(body_html)
|
||||
|
||||
# Replace section content
|
||||
new_body = body_html[:section_start] + new_html + body_html[section_end:]
|
||||
|
||||
# PUT updated page
|
||||
put_data = {
|
||||
"version": {"number": version + 1},
|
||||
"title": title,
|
||||
"type": "page",
|
||||
"body": {
|
||||
"storage": {
|
||||
"value": new_body,
|
||||
"representation": "storage",
|
||||
}
|
||||
},
|
||||
}
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
resp = await client.put(
|
||||
url,
|
||||
json=put_data,
|
||||
auth=(CONFLUENCE_USER, CONFLUENCE_TOKEN),
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return f"Section '{section_heading}' updated successfully."
|
||||
auth = Auth(base_url=CONFLUENCE_URL, username=CONFLUENCE_USER, api_token=CONFLUENCE_TOKEN)
|
||||
result = await section_update(page_id, section_heading, new_html, auth)
|
||||
if result.ok:
|
||||
msg = f"Section '{section_heading}' updated successfully."
|
||||
if result.retries > 0:
|
||||
msg += f" ({result.retries} conflict retries)"
|
||||
return msg
|
||||
return result.message
|
||||
|
||||
|
||||
def _build_e2ee_options() -> rtc.E2EEOptions:
|
||||
|
||||
Reference in New Issue
Block a user