Compare commits
14 Commits
v2026.01.0
...
v2026.01.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbfce27986 | ||
|
|
9be6fe08fa | ||
|
|
782378eed8 | ||
|
|
4e59bb6518 | ||
|
|
3e73fcb3f0 | ||
|
|
c460337c43 | ||
|
|
e775b23503 | ||
|
|
b3cdb8e26e | ||
|
|
0e6f902d16 | ||
|
|
c15c73897f | ||
|
|
035439ce02 | ||
|
|
b84ff4a3a2 | ||
|
|
e22744abd0 | ||
|
|
54c90238f7 |
@@ -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.
|
||||||
|
|
||||||
|
|||||||
42
.github/workflows/release.yml
vendored
42
.github/workflows/release.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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` 设置,用于控制文件名生成策略。
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 标题生成文件名。
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|
||||||
if not tables:
|
# Determine messages to process based on scope
|
||||||
raise HTTPException(status_code=400, detail="No tables found.")
|
target_messages = []
|
||||||
|
if self.valves.EXPORT_SCOPE == "all_messages":
|
||||||
|
target_messages = messages
|
||||||
|
else:
|
||||||
|
target_messages = [messages[-1]]
|
||||||
|
|
||||||
# Generate filename
|
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:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 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,7 +939,28 @@ class Action:
|
|||||||
# Text - Left aligned
|
# Text - Left aligned
|
||||||
current_format = text_format
|
current_format = text_format
|
||||||
|
|
||||||
worksheet.write(row_idx + 1, col_idx, value, current_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)
|
||||||
|
|
||||||
# 4. Auto-adjust column width
|
# 4. Auto-adjust column width
|
||||||
for col_idx, column in enumerate(headers):
|
for col_idx, column in enumerate(headers):
|
||||||
@@ -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)}")
|
||||||
|
|||||||
@@ -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="未找到消息。")
|
||||||
|
|
||||||
if not tables:
|
# Determine messages to process based on scope
|
||||||
raise HTTPException(status_code=400, detail="未找到任何表格。")
|
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:
|
||||||
|
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,7 +944,28 @@ class Action:
|
|||||||
# 文本类型 - 左对齐
|
# 文本类型 - 左对齐
|
||||||
current_format = text_format
|
current_format = text_format
|
||||||
|
|
||||||
worksheet.write(row_idx + 1, col_idx, value, current_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)
|
||||||
|
|
||||||
# 4. 自动调整列宽
|
# 4. 自动调整列宽
|
||||||
for col_idx, column in enumerate(headers):
|
for col_idx, column in enumerate(headers):
|
||||||
@@ -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)}")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user