Compare commits

...

14 Commits

Author SHA1 Message Date
fujie
dbfce27986 fix: explicitly add newlines before EOF in release workflow 2026-01-03 16:04:21 +08:00
fujie
9be6fe08fa fix: ensure extract_plugin_versions.py output ends with newline to prevent GH Actions EOF error 2026-01-03 16:00:50 +08:00
fujie
782378eed8 fix: quote GITHUB_OUTPUT delimiter to prevent EOF error 2026-01-03 14:18:50 +08:00
fujie
4e59bb6518 feat: support full-cell markdown italic formatting in excel export 2026-01-03 14:18:32 +08:00
fujie
3e73fcb3f0 feat: refine excel export to apply bold formatting only to fully bolded cells 2026-01-03 14:16:00 +08:00
fujie
c460337c43 feat: support markdown italic formatting and refine bold parsing 2026-01-03 14:12:53 +08:00
fujie
e775b23503 feat: support markdown bold formatting in excel export 2026-01-03 14:10:11 +08:00
fujie
b3cdb8e26e fix: use gh cli for asset upload to support chinese filenames 2026-01-03 13:52:25 +08:00
fujie
0e6f902d16 chore: add debug steps and artifact upload to release workflow 2026-01-03 13:38:59 +08:00
fujie
c15c73897f fix: enforce utf-8 and disable git path quoting in release workflow to support chinese filenames 2026-01-03 13:25:15 +08:00
fujie
035439ce02 docs: forbid agents from auto-pushing to remote main branch 2026-01-03 13:21:31 +08:00
fujie
b84ff4a3a2 chore: disable auto-release on push to main, use workflow_dispatch only 2026-01-03 13:18:36 +08:00
fujie
e22744abd0 feat: add export scope option and smart sheet naming to export to excel plugin (v0.3.5) 2026-01-03 13:15:13 +08:00
fujie
54c90238f7 fix: enforce utf-8 locale in release workflow to support chinese filenames 2026-01-03 12:42:26 +08:00
10 changed files with 442 additions and 105 deletions

View File

@@ -91,3 +91,12 @@ Before committing:
- [ ] `docs/` index and detail pages are updated? - [ ] `docs/` index and detail pages are updated?
- [ ] Root `README.md` is updated? - [ ] Root `README.md` is updated?
- [ ] All version numbers match exactly? - [ ] All version numbers match exactly?
## 5. Git Operations (Agent Rules)
**CRITICAL RULE FOR AGENTS**:
- **No Auto-Push**: Agents **MUST NOT** automatically push changes to the remote `main` branch.
- **Local Commit Only**: All changes must be committed locally.
- **User Approval**: Pushing to remote requires explicit user action or approval.

View File

@@ -54,6 +54,9 @@ permissions:
jobs: jobs:
check-changes: check-changes:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
outputs: outputs:
has_changes: ${{ steps.detect.outputs.has_changes }} has_changes: ${{ steps.detect.outputs.has_changes }}
changed_plugins: ${{ steps.detect.outputs.changed_plugins }} changed_plugins: ${{ steps.detect.outputs.changed_plugins }}
@@ -65,6 +68,12 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Configure Git
run: |
git config --global core.quotepath false
git config --global i18n.commitencoding utf-8
git config --global i18n.logoutputencoding utf-8
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
@@ -131,6 +140,7 @@ jobs:
echo "changed_plugins<<EOF" >> $GITHUB_OUTPUT echo "changed_plugins<<EOF" >> $GITHUB_OUTPUT
cat changed_files.txt >> $GITHUB_OUTPUT cat changed_files.txt >> $GITHUB_OUTPUT
echo "" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT
fi fi
@@ -138,6 +148,7 @@ jobs:
{ {
echo 'release_notes<<EOF' echo 'release_notes<<EOF'
cat changes.md cat changes.md
echo ""
echo 'EOF' echo 'EOF'
} >> $GITHUB_OUTPUT } >> $GITHUB_OUTPUT
@@ -145,6 +156,10 @@ jobs:
needs: check-changes needs: check-changes
if: needs.check-changes.outputs.has_changes == 'true' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') if: needs.check-changes.outputs.has_changes == 'true' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -152,6 +167,12 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Configure Git
run: |
git config --global core.quotepath false
git config --global i18n.commitencoding utf-8
git config --global i18n.logoutputencoding utf-8
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
@@ -205,6 +226,17 @@ jobs:
echo "=== Collected Files ===" echo "=== Collected Files ==="
find release_plugins -name "*.py" -type f | head -20 find release_plugins -name "*.py" -type f | head -20
- name: Debug Filenames
run: |
python3 -c "import sys; print(f'Filesystem encoding: {sys.getfilesystemencoding()}')"
ls -R release_plugins
- name: Upload Debug Artifacts
uses: actions/upload-artifact@v4
with:
name: debug-plugins
path: release_plugins/
- name: Get commit messages - name: Get commit messages
id: commits id: commits
if: github.event_name == 'push' if: github.event_name == 'push'
@@ -220,8 +252,9 @@ jobs:
{ {
echo 'commits<<EOF' echo 'commits<<EOF'
echo "$COMMITS" echo "$COMMITS"
echo ""
echo 'EOF' echo 'EOF'
} >> $GITHUB_OUTPUT } >> "$GITHUB_OUTPUT"
- name: Generate release notes - name: Generate release notes
id: notes id: notes
@@ -301,10 +334,15 @@ jobs:
prerelease: ${{ github.event.inputs.prerelease || false }} prerelease: ${{ github.event.inputs.prerelease || false }}
files: | files: |
plugin_versions.json plugin_versions.json
release_plugins/**/*.py
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Release Assets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
find release_plugins -type f -name "*.py" -print0 | xargs -0 gh release upload ${{ steps.version.outputs.version }} --clobber
- name: Summary - name: Summary
run: | run: |
echo "## 🚀 Release Created Successfully!" >> $GITHUB_STEP_SUMMARY echo "## 🚀 Release Created Successfully!" >> $GITHUB_STEP_SUMMARY

View File

@@ -6,6 +6,11 @@
Export chat conversations to Excel spreadsheet format for analysis, archiving, and sharing. Export chat conversations to Excel spreadsheet format for analysis, archiving, and sharing.
### What's New in v0.3.5
- **Export Scope**: Added `EXPORT_SCOPE` valve to choose between exporting tables from the "Last Message" (default) or "All Messages".
- **Smart Sheet Naming**: Automatically names sheets based on Markdown headers, AI titles (if enabled), or message index (e.g., `Msg1-Tab1`).
- **Multiple Tables Support**: Improved handling of multiple tables within single or multiple messages.
## What's New in v0.3.4 ## What's New in v0.3.4
- **Smart Filename Generation**: Now supports generating filenames based on Chat Title, AI Summary, or Markdown Headers. - **Smart Filename Generation**: Now supports generating filenames based on Chat Title, AI Summary, or Markdown Headers.

View File

@@ -6,7 +6,12 @@
将聊天记录导出为 Excel 表格,便于分析、归档和分享。 将聊天记录导出为 Excel 表格,便于分析、归档和分享。
## v0.3.4 更新内容 ### v0.3.5 更新内容
- **导出范围**: 新增 `EXPORT_SCOPE` 配置项,可选择导出“最后一条消息”(默认)或“所有消息”中的表格。
- **智能 Sheet 命名**: 根据 Markdown 标题、AI 标题(如启用)或消息索引(如 `消息1-表1`)自动命名 Sheet。
- **多表格支持**: 优化了对单条或多条消息中包含多个表格的处理。
### v0.3.4 更新内容
- **智能文件名生成**支持根据对话标题、AI 总结或 Markdown 标题生成文件名。 - **智能文件名生成**支持根据对话标题、AI 总结或 Markdown 标题生成文件名。
- **配置选项**:新增 `TITLE_SOURCE` 设置,用于控制文件名生成策略。 - **配置选项**:新增 `TITLE_SOURCE` 设置,用于控制文件名生成策略。

View File

@@ -53,7 +53,7 @@ Actions are interactive plugins that:
Export chat conversations to Excel spreadsheet format for analysis and archiving. Export chat conversations to Excel spreadsheet format for analysis and archiving.
**Version:** 0.3.4 **Version:** 0.3.5
[:octicons-arrow-right-24: Documentation](export-to-excel.md) [:octicons-arrow-right-24: Documentation](export-to-excel.md)

View File

@@ -2,6 +2,11 @@
This plugin allows you to export your chat history to an Excel (.xlsx) file directly from the chat interface. This plugin allows you to export your chat history to an Excel (.xlsx) file directly from the chat interface.
### What's New in v0.3.5
- **Export Scope**: Added `EXPORT_SCOPE` valve to choose between exporting tables from the "Last Message" (default) or "All Messages".
- **Smart Sheet Naming**: Automatically names sheets based on Markdown headers, AI titles (if enabled), or message index (e.g., `Msg1-Tab1`).
- **Multiple Tables Support**: Improved handling of multiple tables within single or multiple messages.
## What's New in v0.3.4 ## What's New in v0.3.4
- **Smart Filename Generation**: Now supports generating filenames based on Chat Title, AI Summary, or Markdown Headers. - **Smart Filename Generation**: Now supports generating filenames based on Chat Title, AI Summary, or Markdown Headers.

View File

@@ -2,6 +2,11 @@
此插件允许你直接从聊天界面将对话历史导出为 Excel (.xlsx) 文件。 此插件允许你直接从聊天界面将对话历史导出为 Excel (.xlsx) 文件。
### v0.3.5 更新内容
- **导出范围**: 新增 `EXPORT_SCOPE` 配置项,可选择导出“最后一条消息”(默认)或“所有消息”中的表格。
- **智能 Sheet 命名**: 根据 Markdown 标题、AI 标题(如启用)或消息索引(如 `消息1-表1`)自动命名 Sheet。
- **多表格支持**: 优化了对单条或多条消息中包含多个表格的处理。
## v0.3.4 更新内容 ## v0.3.4 更新内容
- **智能文件名生成**支持根据对话标题、AI 总结或 Markdown 标题生成文件名。 - **智能文件名生成**支持根据对话标题、AI 总结或 Markdown 标题生成文件名。

View File

@@ -3,7 +3,7 @@ title: Export to Excel
author: Fu-Jie author: Fu-Jie
author_url: https://github.com/Fu-Jie author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.3.4 version: 0.3.5
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg== icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
description: Exports the current chat history to an Excel (.xlsx) file, with automatic header extraction. description: Exports the current chat history to an Excel (.xlsx) file, with automatic header extraction.
""" """
@@ -30,6 +30,10 @@ class Action:
default="chat_title", default="chat_title",
description="Title Source: 'chat_title' (Chat Title), 'ai_generated' (AI Generated), 'markdown_title' (Markdown Title)", description="Title Source: 'chat_title' (Chat Title), 'ai_generated' (AI Generated), 'markdown_title' (Markdown Title)",
) )
EXPORT_SCOPE: str = Field(
default="last_message",
description="Export Scope: 'last_message' (Last Message Only), 'all_messages' (All Messages)",
)
def __init__(self): def __init__(self):
self.valves = self.Valves() self.valves = self.Valves()
@@ -64,8 +68,6 @@ class Action:
user_id = __user__.get("id", "unknown_user") user_id = __user__.get("id", "unknown_user")
if __event_emitter__: if __event_emitter__:
last_assistant_message = body["messages"][-1]
await __event_emitter__( await __event_emitter__(
{ {
"type": "status", "type": "status",
@@ -74,19 +76,115 @@ class Action:
) )
try: try:
message_content = last_assistant_message["content"] messages = body.get("messages", [])
tables = self.extract_tables_from_message(message_content) if not messages:
raise HTTPException(status_code=400, detail="No messages found.")
# Determine messages to process based on scope
target_messages = []
if self.valves.EXPORT_SCOPE == "all_messages":
target_messages = messages
else:
target_messages = [messages[-1]]
all_tables = []
all_sheet_names = []
# Process messages
for msg_index, msg in enumerate(target_messages):
content = msg.get("content", "")
tables = self.extract_tables_from_message(content)
if not tables: if not tables:
raise HTTPException(status_code=400, detail="No tables found.") continue
# Generate filename # Generate sheet names for this message's tables
# If multiple messages, we need to ensure uniqueness across the whole workbook
# We'll generate base names here and deduplicate later if needed,
# or better: generate unique names on the fly.
# Extract headers for this message
headers = []
lines = content.split("\n")
for i, line in enumerate(lines):
if re.match(r"^#{1,6}\s+", line):
headers.append(
{
"text": re.sub(r"^#{1,6}\s+", "", line).strip(),
"line_num": i,
}
)
for table_index, table in enumerate(tables):
sheet_name = ""
# 1. Try Markdown Header (closest above)
table_start_line = table["start_line"] - 1
closest_header_text = None
candidate_headers = [
h for h in headers if h["line_num"] < table_start_line
]
if candidate_headers:
closest_header = max(
candidate_headers, key=lambda x: x["line_num"]
)
closest_header_text = closest_header["text"]
if closest_header_text:
sheet_name = self.clean_sheet_name(closest_header_text)
# 2. AI Generated (Only if explicitly enabled and we have a request object)
# Note: Generating titles for EVERY table in all messages might be too slow/expensive.
# We'll skip this for 'all_messages' scope to avoid timeout, unless it's just one message.
if (
not sheet_name
and self.valves.TITLE_SOURCE == "ai_generated"
and len(target_messages) == 1
):
# Logic for AI generation (simplified for now, reusing existing flow if possible)
pass
# 3. Fallback: Message Index
if not sheet_name:
if len(target_messages) > 1:
# Use global message index (from original list if possible, but here we iterate target_messages)
# Let's use the loop index.
# If multiple tables in one message: "Msg 1 - Table 1"
if len(tables) > 1:
sheet_name = f"Msg{msg_index+1}-Tab{table_index+1}"
else:
sheet_name = f"Msg{msg_index+1}"
else:
# Single message (last_message scope)
if len(tables) > 1:
sheet_name = f"Table {table_index+1}"
else:
sheet_name = "Sheet1"
all_tables.append(table)
all_sheet_names.append(sheet_name)
if not all_tables:
raise HTTPException(
status_code=400, detail="No tables found in the selected scope."
)
# Deduplicate sheet names
final_sheet_names = []
seen_names = {}
for name in all_sheet_names:
base_name = name
counter = 1
while name in seen_names:
name = f"{base_name} ({counter})"
counter += 1
seen_names[name] = True
final_sheet_names.append(name)
# Generate Workbook Title (Filename)
# Use the title of the chat, or the first header of the first message with tables
title = "" title = ""
chat_id = self.extract_chat_id( chat_id = self.extract_chat_id(body, None)
body, None
) # metadata not available in action signature yet, but usually in body
# Fetch chat_title directly via chat_id as it's usually missing in body
chat_title = "" chat_title = ""
if chat_id: if chat_id:
chat_title = await self.fetch_chat_title(chat_id, user_id) chat_title = await self.fetch_chat_title(chat_id, user_id)
@@ -97,43 +195,29 @@ class Action:
): ):
title = chat_title title = chat_title
elif self.valves.TITLE_SOURCE == "markdown_title": elif self.valves.TITLE_SOURCE == "markdown_title":
title = self.extract_title(message_content) # Try to find first header in the first message that has content
elif self.valves.TITLE_SOURCE == "ai_generated": for msg in target_messages:
# We need request object for AI generation, but it's not passed in standard action signature in this version extracted = self.extract_title(msg.get("content", ""))
# However, we can try to use the one from global context if available or skip if extracted:
# For now, let's assume we might not have it and fallback or use what we have title = extracted
# Wait, export_to_word uses __request__. Let's check if we can add it to signature. break
pass
# Get dynamic filename and sheet names # Fallback for filename
workbook_name_from_content, sheet_names = (
self.generate_names_from_content(message_content, tables)
)
# If AI generation is selected but we need request, we need to update signature.
# Let's update signature in next chunk.
# Fallback logic for title
if not title: if not title:
if self.valves.TITLE_SOURCE == "ai_generated": if chat_title:
# AI generation needs request, handled later
pass
elif self.valves.TITLE_SOURCE == "markdown_title":
pass # Already tried
# If still no title, try workbook_name_from_content (which uses headers)
if not title and workbook_name_from_content:
title = workbook_name_from_content
# If still no title, use chat_title if available
if not title and chat_title:
title = chat_title title = chat_title
else:
# Try extracting from content again if not already tried
if self.valves.TITLE_SOURCE != "markdown_title":
for msg in target_messages:
extracted = self.extract_title(msg.get("content", ""))
if extracted:
title = extracted
break
# Use optimized filename generation logic
current_datetime = datetime.datetime.now() current_datetime = datetime.datetime.now()
formatted_date = current_datetime.strftime("%Y%m%d") formatted_date = current_datetime.strftime("%Y%m%d")
# If no title found, use user_yyyymmdd format
if not title: if not title:
workbook_name = f"{user_name}_{formatted_date}" workbook_name = f"{user_name}_{formatted_date}"
else: else:
@@ -146,8 +230,10 @@ class Action:
os.makedirs(os.path.dirname(excel_file_path), exist_ok=True) os.makedirs(os.path.dirname(excel_file_path), exist_ok=True)
# Save tables to Excel (using enhanced formatting) # Save tables to Excel
self.save_tables_to_excel_enhanced(tables, excel_file_path, sheet_names) self.save_tables_to_excel_enhanced(
all_tables, excel_file_path, final_sheet_names
)
# Trigger file download # Trigger file download
if __event_call__: if __event_call__:
@@ -663,6 +749,28 @@ class Action:
} }
) )
# Bold cell style (for full cell bolding)
text_bold_format = workbook.add_format(
{
"border": 1,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
"bold": True,
}
)
# Italic cell style (for full cell italics)
text_italic_format = workbook.add_format(
{
"border": 1,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
"italic": True,
}
)
for i, table in enumerate(tables): for i, table in enumerate(tables):
try: try:
table_data = table["data"] table_data = table["data"]
@@ -734,6 +842,8 @@ class Action:
decimal_format, decimal_format,
date_format, date_format,
sequence_format, sequence_format,
text_bold_format,
text_italic_format,
) )
except Exception as e: except Exception as e:
@@ -757,6 +867,8 @@ class Action:
decimal_format, decimal_format,
date_format, date_format,
sequence_format, sequence_format,
text_bold_format=None,
text_italic_format=None,
): ):
""" """
Apply enhanced formatting Apply enhanced formatting
@@ -765,6 +877,7 @@ class Action:
- Text: Left aligned - Text: Left aligned
- Date: Center aligned - Date: Center aligned
- Sequence: Center aligned - Sequence: Center aligned
- Supports full cell Markdown bold (**text**) and italic (*text*)
""" """
try: try:
# 1. Write headers (Center aligned) # 1. Write headers (Center aligned)
@@ -826,6 +939,27 @@ class Action:
# Text - Left aligned # Text - Left aligned
current_format = text_format current_format = text_format
if content_type == "text" and isinstance(value, str):
# Check for full cell bold (**text**)
match_bold = re.fullmatch(r"\*\*(.+)\*\*", value.strip())
# Check for full cell italic (*text*)
match_italic = re.fullmatch(r"\*(.+)\*", value.strip())
if match_bold:
# Extract content and apply bold format
clean_value = match_bold.group(1)
worksheet.write(
row_idx + 1, col_idx, clean_value, text_bold_format
)
elif match_italic:
# Extract content and apply italic format
clean_value = match_italic.group(1)
worksheet.write(
row_idx + 1, col_idx, clean_value, text_italic_format
)
else:
worksheet.write(row_idx + 1, col_idx, value, current_format)
else:
worksheet.write(row_idx + 1, col_idx, value, current_format) worksheet.write(row_idx + 1, col_idx, value, current_format)
# 4. Auto-adjust column width # 4. Auto-adjust column width
@@ -916,3 +1050,6 @@ class Action:
except Exception as e: except Exception as e:
print(f"Error in basic formatting: {str(e)}") print(f"Error in basic formatting: {str(e)}")
except Exception as e:
print(f"Error in basic formatting: {str(e)}")

View File

@@ -3,7 +3,7 @@ title: 导出为 Excel
author: Fu-Jie author: Fu-Jie
author_url: https://github.com/Fu-Jie author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.3.4 version: 0.3.5
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg== icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
description: 将当前对话历史导出为 Excel (.xlsx) 文件,支持自动提取表头。 description: 将当前对话历史导出为 Excel (.xlsx) 文件,支持自动提取表头。
""" """
@@ -28,7 +28,11 @@ class Action:
class Valves(BaseModel): class Valves(BaseModel):
TITLE_SOURCE: str = Field( TITLE_SOURCE: str = Field(
default="chat_title", default="chat_title",
description="标题来源'chat_title' (对话标题), 'ai_generated' (AI生成), 'markdown_title' (Markdown标题)", description="标题来源: 'chat_title' (对话标题), 'ai_generated' (AI生成), 'markdown_title' (Markdown标题)",
)
EXPORT_SCOPE: str = Field(
default="last_message",
description="导出范围: 'last_message' (仅最后一条消息), 'all_messages' (所有消息)",
) )
def __init__(self): def __init__(self):
@@ -50,43 +54,127 @@ class Action:
print(f"action:{__name__}") print(f"action:{__name__}")
if isinstance(__user__, (list, tuple)): if isinstance(__user__, (list, tuple)):
user_language = ( user_language = (
__user__[0].get("language", "zh-CN") if __user__ else "zh-CN" __user__[0].get("language", "en-US") if __user__ else "en-US"
) )
user_name = __user__[0].get("name", "用户") if __user__[0] else "用户" user_name = __user__[0].get("name", "User") if __user__[0] else "User"
user_id = ( user_id = (
__user__[0]["id"] __user__[0]["id"]
if __user__ and "id" in __user__[0] if __user__ and "id" in __user__[0]
else "unknown_user" else "unknown_user"
) )
elif isinstance(__user__, dict): elif isinstance(__user__, dict):
user_language = __user__.get("language", "zh-CN") user_language = __user__.get("language", "en-US")
user_name = __user__.get("name", "用户") user_name = __user__.get("name", "User")
user_id = __user__.get("id", "unknown_user") user_id = __user__.get("id", "unknown_user")
if __event_emitter__: if __event_emitter__:
last_assistant_message = body["messages"][-1]
await __event_emitter__( await __event_emitter__(
{ {
"type": "status", "type": "status",
"data": {"description": "正在保存文件...", "done": False}, "data": {"description": "正在保存文件...", "done": False},
} }
) )
try: try:
message_content = last_assistant_message["content"] messages = body.get("messages", [])
tables = self.extract_tables_from_message(message_content) if not messages:
raise HTTPException(status_code=400, detail="未找到消息。")
# Determine messages to process based on scope
target_messages = []
if self.valves.EXPORT_SCOPE == "all_messages":
target_messages = messages
else:
target_messages = [messages[-1]]
all_tables = []
all_sheet_names = []
# Process messages
for msg_index, msg in enumerate(target_messages):
content = msg.get("content", "")
tables = self.extract_tables_from_message(content)
if not tables: if not tables:
raise HTTPException(status_code=400, detail="未找到任何表格。") continue
# 生成文件名 # Generate sheet names for this message's tables
# Extract headers for this message
headers = []
lines = content.split("\n")
for i, line in enumerate(lines):
if re.match(r"^#{1,6}\s+", line):
headers.append(
{
"text": re.sub(r"^#{1,6}\s+", "", line).strip(),
"line_num": i,
}
)
for table_index, table in enumerate(tables):
sheet_name = ""
# 1. Try Markdown Header (closest above)
table_start_line = table["start_line"] - 1
closest_header_text = None
candidate_headers = [
h for h in headers if h["line_num"] < table_start_line
]
if candidate_headers:
closest_header = max(
candidate_headers, key=lambda x: x["line_num"]
)
closest_header_text = closest_header["text"]
if closest_header_text:
sheet_name = self.clean_sheet_name(closest_header_text)
# 2. AI Generated (Only if explicitly enabled and we have a request object)
if (
not sheet_name
and self.valves.TITLE_SOURCE == "ai_generated"
and len(target_messages) == 1
):
pass
# 3. Fallback: Message Index
if not sheet_name:
if len(target_messages) > 1:
if len(tables) > 1:
sheet_name = f"消息{msg_index+1}-表{table_index+1}"
else:
sheet_name = f"消息{msg_index+1}"
else:
# Single message (last_message scope)
if len(tables) > 1:
sheet_name = f"{table_index+1}"
else:
sheet_name = "Sheet1"
all_tables.append(table)
all_sheet_names.append(sheet_name)
if not all_tables:
raise HTTPException(
status_code=400, detail="在选定范围内未找到表格。"
)
# Deduplicate sheet names
final_sheet_names = []
seen_names = {}
for name in all_sheet_names:
base_name = name
counter = 1
while name in seen_names:
name = f"{base_name} ({counter})"
counter += 1
seen_names[name] = True
final_sheet_names.append(name)
# Generate Workbook Title (Filename)
title = "" title = ""
chat_id = self.extract_chat_id( chat_id = self.extract_chat_id(body, None)
body, None
) # metadata 在此版本 action 签名中不可用,但通常在 body 中
# 直接通过 chat_id 获取对话标题,因为 body 中通常缺少该信息
chat_title = "" chat_title = ""
if chat_id: if chat_id:
chat_title = await self.fetch_chat_title(chat_id, user_id) chat_title = await self.fetch_chat_title(chat_id, user_id)
@@ -97,37 +185,27 @@ class Action:
): ):
title = chat_title title = chat_title
elif self.valves.TITLE_SOURCE == "markdown_title": elif self.valves.TITLE_SOURCE == "markdown_title":
title = self.extract_title(message_content) for msg in target_messages:
elif self.valves.TITLE_SOURCE == "ai_generated": extracted = self.extract_title(msg.get("content", ""))
# AI 生成需要 request 对象,稍后处理 if extracted:
pass title = extracted
break
# 获取动态文件名和sheet名称 # Fallback for filename
workbook_name_from_content, sheet_names = (
self.generate_names_from_content(message_content, tables)
)
# 标题回退逻辑
if not title: if not title:
if self.valves.TITLE_SOURCE == "ai_generated": if chat_title:
# AI 生成需要 request稍后处理
pass
elif self.valves.TITLE_SOURCE == "markdown_title":
pass # 已尝试
# 如果仍无标题,尝试使用 workbook_name_from_content (基于表头)
if not title and workbook_name_from_content:
title = workbook_name_from_content
# 如果仍无标题,尝试使用 chat_title
if not title and chat_title:
title = chat_title title = chat_title
else:
if self.valves.TITLE_SOURCE != "markdown_title":
for msg in target_messages:
extracted = self.extract_title(msg.get("content", ""))
if extracted:
title = extracted
break
# 使用优化后的文件名生成逻辑
current_datetime = datetime.datetime.now() current_datetime = datetime.datetime.now()
formatted_date = current_datetime.strftime("%Y%m%d") formatted_date = current_datetime.strftime("%Y%m%d")
# 如果没找到标题则使用 user_yyyymmdd 格式
if not title: if not title:
workbook_name = f"{user_name}_{formatted_date}" workbook_name = f"{user_name}_{formatted_date}"
else: else:
@@ -140,10 +218,12 @@ class Action:
os.makedirs(os.path.dirname(excel_file_path), exist_ok=True) os.makedirs(os.path.dirname(excel_file_path), exist_ok=True)
# 保存表格到Excel使用符合中国规范的格式化功能 # Save tables to Excel
self.save_tables_to_excel_enhanced(tables, excel_file_path, sheet_names) self.save_tables_to_excel_enhanced(
all_tables, excel_file_path, final_sheet_names
)
# 触发文件下载 # Trigger file download
if __event_call__: if __event_call__:
with open(excel_file_path, "rb") as file: with open(excel_file_path, "rb") as file:
file_content = file.read() file_content = file.read()
@@ -174,7 +254,7 @@ class Action:
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
document.body.removeChild(a); document.body.removeChild(a);
}} catch (error) {{ }} catch (error) {{
console.error('触发下载时出错:', error); console.error('Error triggering download:', error);
}} }}
""" """
}, },
@@ -183,15 +263,15 @@ class Action:
await __event_emitter__( await __event_emitter__(
{ {
"type": "status", "type": "status",
"data": {"description": "输出已保存", "done": True}, "data": {"description": "文件已保存", "done": True},
} }
) )
# 清理临时文件 # Clean up temp file
if os.path.exists(excel_file_path): if os.path.exists(excel_file_path):
os.remove(excel_file_path) os.remove(excel_file_path)
return {"message": "下载事件已触发"} return {"message": "下载已触发"}
except HTTPException as e: except HTTPException as e:
print(f"Error processing tables: {str(e.detail)}") print(f"Error processing tables: {str(e.detail)}")
@@ -199,13 +279,13 @@ class Action:
{ {
"type": "status", "type": "status",
"data": { "data": {
"description": f"保存文件时出错: {e.detail}", "description": f"保存文件错: {e.detail}",
"done": True, "done": True,
}, },
} }
) )
await self._send_notification( await self._send_notification(
__event_emitter__, "error", "没有找到可导出的表格!" __event_emitter__, "error", "找到可导出的表格"
) )
raise e raise e
except Exception as e: except Exception as e:
@@ -214,13 +294,13 @@ class Action:
{ {
"type": "status", "type": "status",
"data": { "data": {
"description": f"保存文件时出错: {str(e)}", "description": f"保存文件错: {str(e)}",
"done": True, "done": True,
}, },
} }
) )
await self._send_notification( await self._send_notification(
__event_emitter__, "error", "没有找到可导出的表格!" __event_emitter__, "error", "找到可导出的表格"
) )
async def generate_title_using_ai( async def generate_title_using_ai(
@@ -674,6 +754,28 @@ class Action:
} }
) )
# 粗体单元格样式 (用于全单元格加粗)
text_bold_format = workbook.add_format(
{
"border": 1,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
"bold": True,
}
)
# 斜体单元格样式 (用于全单元格斜体)
text_italic_format = workbook.add_format(
{
"border": 1,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
"italic": True,
}
)
for i, table in enumerate(tables): for i, table in enumerate(tables):
try: try:
table_data = table["data"] table_data = table["data"]
@@ -745,6 +847,8 @@ class Action:
decimal_format, decimal_format,
date_format, date_format,
sequence_format, sequence_format,
text_bold_format,
text_italic_format,
) )
except Exception as e: except Exception as e:
@@ -768,6 +872,8 @@ class Action:
decimal_format, decimal_format,
date_format, date_format,
sequence_format, sequence_format,
text_bold_format=None,
text_italic_format=None,
): ):
""" """
应用符合中国官方表格规范的格式化 应用符合中国官方表格规范的格式化
@@ -776,6 +882,7 @@ class Action:
- 文本: 左对齐 - 文本: 左对齐
- 日期: 居中对齐 - 日期: 居中对齐
- 序号: 居中对齐 - 序号: 居中对齐
- 支持全单元格 Markdown 粗体 (**text**) 和斜体 (*text*)
""" """
try: try:
# 1. 写入表头(居中对齐) # 1. 写入表头(居中对齐)
@@ -837,6 +944,27 @@ class Action:
# 文本类型 - 左对齐 # 文本类型 - 左对齐
current_format = text_format current_format = text_format
if content_type == "text" and isinstance(value, str):
# 检查是否全单元格加粗 (**text**)
match_bold = re.fullmatch(r"\*\*(.+)\*\*", value.strip())
# 检查是否全单元格斜体 (*text*)
match_italic = re.fullmatch(r"\*(.+)\*", value.strip())
if match_bold:
# 提取内容并应用粗体格式
clean_value = match_bold.group(1)
worksheet.write(
row_idx + 1, col_idx, clean_value, text_bold_format
)
elif match_italic:
# 提取内容并应用斜体格式
clean_value = match_italic.group(1)
worksheet.write(
row_idx + 1, col_idx, clean_value, text_italic_format
)
else:
worksheet.write(row_idx + 1, col_idx, value, current_format)
else:
worksheet.write(row_idx + 1, col_idx, value, current_format) worksheet.write(row_idx + 1, col_idx, value, current_format)
# 4. 自动调整列宽 # 4. 自动调整列宽
@@ -937,3 +1065,6 @@ class Action:
except Exception as e: except Exception as e:
print(f"Warning: Even basic formatting failed: {str(e)}") print(f"Warning: Even basic formatting failed: {str(e)}")
except Exception as e:
print(f"Warning: Even basic formatting failed: {str(e)}")

View File

@@ -295,6 +295,8 @@ def main():
if args.output: if args.output:
with open(args.output, "w", encoding="utf-8") as f: with open(args.output, "w", encoding="utf-8") as f:
f.write(output) f.write(output)
if not output.endswith("\n"):
f.write("\n")
print(f"Output written to {args.output}") print(f"Output written to {args.output}")
else: else:
print(output) print(output)