feat(export-to-word): add full chat export with URLs, stats, and session info#93
feat(export-to-word): add full chat export with URLs, stats, and session info#93Fu-Jie wants to merge 1 commit into
Conversation
…ion info Extend the Export to Word Enhanced plugin to optionally export the complete conversation (all user prompts and assistant replies) instead of only the last assistant message, as requested in #84. New valves: - EXPORT_FULL_CHAT: toggle full chat export mode - FULL_CHAT_SKIP_SYSTEM: skip system-role messages - FULL_CHAT_INCLUDE_STATS: per-message statistics table (model, tokens) - FULL_CHAT_INCLUDE_SESSION_INFO: session summary (title, counts, export time) - FULL_CHAT_INCLUDE_URLS: collected URLs section with clickable hyperlinks The full chat export iterates over body["messages"], labels each message by role (User/Assistant), collects sources/URLs from all assistant messages and message text, and appends supplementary sections after the main content. All new valves are also exposed via UserValves for per-user overrides. Closes #84
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
❌ Plugin Version Check / 插件版本检查未检测到版本更新!修改插件文件时必须更新版本号。 No version update detected! You must update the version number when modifying plugin files. This comment was generated automatically. / 此评论由自动生成。 |
There was a problem hiding this comment.
Code Review
本拉取请求为 Word 导出插件引入了“完整对话导出”功能(EXPORT_FULL_CHAT),支持导出包含所有用户和助手消息的完整对话,并附带消息统计、收集的链接及会话信息摘要。审查意见均非常中肯,主要提出了以下优化建议:移除 _export_full_chat 中的 user_id 和 _add_session_info_section 中的 user_name 等未使用参数;将链接提取中的 re.match 替换为更高效的 startswith;将 list(dict.fromkeys(...));以及在应用表格样式时,使用 try-except KeyError 替代遍历所有样式的列表推导式以提升性能。
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| doc = await self._export_full_chat( | ||
| body=body, | ||
| event_emitter=__event_emitter__, | ||
| user_name=user_name, | ||
| user_id=user_id, | ||
| chat_title=chat_title, | ||
| top_heading=top_heading, | ||
| title=title, | ||
| ) |
There was a problem hiding this comment.
The user_id parameter is passed to _export_full_chat but is never used within that method. It should be removed from the call site to keep the code clean.
doc = await self._export_full_chat(
body=body,
event_emitter=__event_emitter__,
user_name=user_name,
chat_title=chat_title,
top_heading=top_heading,
title=title,
)| def _collect_urls_from_sources(self, sources: List[dict]) -> List[str]: | ||
| """Extract URLs from Open WebUI RAG source dicts.""" | ||
| urls: List[str] = [] | ||
| for source in sources or []: | ||
| if not isinstance(source, dict): | ||
| continue | ||
| src_info = source.get("source") or {} | ||
| metadatas = source.get("metadata") or [] | ||
| if not isinstance(metadatas, list): | ||
| metadatas = [] | ||
| for meta in metadatas: | ||
| if not isinstance(meta, dict): | ||
| continue | ||
| url = meta.get("url") or meta.get("source") | ||
| if isinstance(url, str) and re.match(r"^https?://", url) and url not in urls: | ||
| urls.append(url) | ||
| if isinstance(src_info, dict): | ||
| src_urls = src_info.get("urls") | ||
| if isinstance(src_urls, list): | ||
| for u in src_urls: | ||
| if isinstance(u, str) and re.match(r"^https?://", u) and u not in urls: | ||
| urls.append(u) | ||
| src_id = src_info.get("id") | ||
| if isinstance(src_id, str) and re.match(r"^https?://", src_id) and src_id not in urls: | ||
| urls.append(src_id) | ||
| return urls |
There was a problem hiding this comment.
Using re.match(r"^https?://", ...) introduces unnecessary regex compilation and execution overhead. Since you are only checking if the string starts with http:// or https://, using the built-in .startswith(("http://", "https://")) is much faster and more readable.
def _collect_urls_from_sources(self, sources: List[dict]) -> List[str]:
"""Extract URLs from Open WebUI RAG source dicts."""
urls: List[str] = []
for source in sources or []:
if not isinstance(source, dict):
continue
src_info = source.get("source") or {}
metadatas = source.get("metadata") or []
if not isinstance(metadatas, list):
metadatas = []
for meta in metadatas:
if not isinstance(meta, dict):
continue
url = meta.get("url") or meta.get("source")
if isinstance(url, str) and url.startswith(("http://", "https://")) and url not in urls:
urls.append(url)
if isinstance(src_info, dict):
src_urls = src_info.get("urls")
if isinstance(src_urls, list):
for u in src_urls:
if isinstance(u, str) and u.startswith(("http://", "https://")) and u not in urls:
urls.append(u)
src_id = src_info.get("id")
if isinstance(src_id, str) and src_id.startswith(("http://", "https://")) and src_id not in urls:
urls.append(src_id)
return urls| async def _export_full_chat( | ||
| self, | ||
| body: dict, | ||
| event_emitter: Optional[Callable], | ||
| user_name: str, | ||
| user_id: str, | ||
| chat_title: str, | ||
| top_heading: str, | ||
| title: str, | ||
| ) -> Document: |
There was a problem hiding this comment.
| deduped_urls: List[str] = [] | ||
| for u in all_urls: | ||
| if u not in deduped_urls: | ||
| deduped_urls.append(u) |
There was a problem hiding this comment.
De-duplicating a list using a loop with if u not in deduped_urls has list(dict.fromkeys(all_urls)) to de-duplicate the list in
| deduped_urls: List[str] = [] | |
| for u in all_urls: | |
| if u not in deduped_urls: | |
| deduped_urls.append(u) | |
| deduped_urls = list(dict.fromkeys(all_urls)) |
| self._add_session_info_section( | ||
| doc, | ||
| chat_title=chat_title, | ||
| user_name=user_name, | ||
| total_messages=user_count + assistant_count, | ||
| user_count=user_count, | ||
| assistant_count=assistant_count, | ||
| ) |
There was a problem hiding this comment.
The user_name parameter is passed to _add_session_info_section but is never used within that method. It should be removed from the call site.
self._add_session_info_section(
doc,
chat_title=chat_title,
total_messages=user_count + assistant_count,
user_count=user_count,
assistant_count=assistant_count,
)| def _add_session_info_section( | ||
| self, | ||
| doc: Document, | ||
| chat_title: str, | ||
| user_name: str, | ||
| total_messages: int, | ||
| user_count: int, | ||
| assistant_count: int, | ||
| ): | ||
| """Append a session-information summary at the end of the document.""" | ||
| self.add_heading(doc, self._get_msg("session_info"), 2) | ||
|
|
||
| export_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | ||
|
|
||
| rows = [ | ||
| (self._get_msg("chat_title_label"), chat_title or "—"), | ||
| (self._get_msg("total_messages"), str(total_messages)), | ||
| (self._get_msg("user_messages"), str(user_count)), | ||
| (self._get_msg("assistant_messages"), str(assistant_count)), | ||
| (self._get_msg("export_time"), export_time), | ||
| ] | ||
|
|
||
| table = doc.add_table(rows=len(rows), cols=2) | ||
| table.style = "Light Grid Accent 1" if "Light Grid Accent 1" in [s.name for s in doc.styles] else None |
There was a problem hiding this comment.
This refactoring addresses two issues:
- The
user_nameparameter is unused and has been removed from the signature. - Checking
"Light Grid Accent 1" in [s.name for s in doc.styles]iterates over all styles and builds a list every time, which is inefficient. Using atry-except KeyErrorblock is much faster, more idiomatic, and consistent with how styles are applied in_add_message_stats_section.
def _add_session_info_section(
self,
doc: Document,
chat_title: str,
total_messages: int,
user_count: int,
assistant_count: int,
):
"""Append a session-information summary at the end of the document."""
self.add_heading(doc, self._get_msg("session_info"), 2)
export_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rows = [
(self._get_msg("chat_title_label"), chat_title or "—"),
(self._get_msg("total_messages"), str(total_messages)),
(self._get_msg("user_messages"), str(user_count)),
(self._get_msg("assistant_messages"), str(assistant_count)),
(self._get_msg("export_time"), export_time),
]
table = doc.add_table(rows=len(rows), cols=2)
try:
table.style = "Light Grid Accent 1"
except KeyError:
pass
Summary
Implements the complete-chat export feature requested in #84, extending the Export to Word Enhanced plugin so it can export the entire conversation (all user prompts and assistant replies) instead of only the last assistant message.
Background
The current plugin only exports
body["messages"][-1](the last assistant reply). Issue #84 asks for the ability to export the complete chat including:Changes
plugins/actions/export_to_docx/export_to_word.py(+375/-13)New valves (also exposed via
UserValvesfor per-user overrides):EXPORT_FULL_CHATFalseFULL_CHAT_SKIP_SYSTEMTrueFULL_CHAT_INCLUDE_STATSTrueFULL_CHAT_INCLUDE_SESSION_INFOTrueFULL_CHAT_INCLUDE_URLSTrueNew i18n keys (en + zh):
user_label,assistant_label,session_info,collected_urls,message_statistics,export_time,total_messages,user_messages,assistant_messages,chat_title_label,model_label,prompt_tokens,completion_tokens,total_tokens,message_label,no_urls_found,full_chat_converting.New methods:
_export_full_chat()— iterates overbody["messages"], labels each by role (## User / ## Assistant), collects sources/URLs/stats, callsmarkdown_to_docxwith the combined markdown, then appends supplementary sections._extract_message_text()— handles string and multimodal (list-of-parts) content._collect_urls_from_text()— ordered, de-duplicated URL extraction via the existing_AUTO_URL_RE._collect_urls_from_sources()— extracts URLs from Open WebUI RAG source dicts (metadata.url, source.urls, source.id)._add_session_info_section()— 2-column table (title, message counts, export time)._add_collected_urls_section()— numbered list of clickable hyperlinks._add_message_stats_section()— per-message table (#, role, model, prompt/completion/total tokens).action()branching: whenEXPORT_FULL_CHATisTrue, calls_export_full_chat()instead of the single-messagemarkdown_to_docx()path. The existing single-message behavior is fully preserved as the default.plugins/actions/export_to_docx/README.md&README_CN.mdDocumented the new feature in "What's New", "Key Features", and the valves configuration table (both EN and CN).
How It Works
When
EXPORT_FULL_CHATis enabled, the document structure becomes:Test Plan
py_compile)_extract_message_text,_collect_urls_from_text,_collect_urls_from_sources(string / multimodal / dedup / edge cases)EXPORT_FULL_CHAT=FalseenandzhUI languagesBackward Compatibility
The feature is off by default (
EXPORT_FULL_CHAT=False). Existing users see no behavior change until they explicitly enable the new valve.Closes #84