Compare commits

...

3 Commits

11 changed files with 895 additions and 151 deletions

View File

@@ -55,9 +55,13 @@ When adding or updating a plugin, you **MUST** update the following documentatio
Reference: `.github/workflows/release.yml`
### Version Bumping
- **Rule**: Any change to plugin logic **MUST** be accompanied by a version bump in the docstring.
- **Rule**: Version bump is required **ONLY when the user explicitly requests a release**. Regular code changes do NOT require version bumps.
- **Format**: Semantic Versioning (e.g., `1.0.0` -> `1.0.1`).
- **Consistency**: Update version in **ALL** locations:
- **When to Bump**: Only update the version when:
- User says "发布" / "release" / "bump version"
- User explicitly asks to prepare for release
- **Agent Initiative**: After completing significant changes (new features, bug fixes, or multiple code modifications), the agent **SHOULD proactively ask** the user if they want to release a new version. If confirmed, update all version-related files.
- **Consistency**: When bumping, update version in **ALL** locations:
1. English Code (`.py`)
2. Chinese Code (`.py`)
3. English README (`README.md`)
@@ -94,9 +98,6 @@ Before committing:
## 5. Git Operations (Agent Rules)
**CRITICAL RULE FOR AGENTS**:
Strictly follow the rules defined in `.github/copilot-instructions.md`**Git Operations (Agent Rules)** section.
- **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

@@ -833,13 +833,35 @@ For iframe plugins to access parent document theme information, users need to co
### 发布前必须完成 (Pre-release Requirements)
> [!IMPORTANT]
> 版本号**仅在用户明确要求发布时**才需要更新。日常代码更改**无需**更新版本号。
**触发版本更新的关键词**
- 用户说 "发布"、"release"、"bump version"
- 用户明确要求准备发布
**Agent 主动询问发布 (Agent-Initiated Release Prompt)**
当 Agent 完成以下类型的更改后,**应主动询问**用户是否需要发布新版本:
| 更改类型 | 示例 | 是否询问发布 |
|---------|------|-------------|
| 新功能 | 新增导出格式、新的配置选项 | ✅ 询问 |
| 重要 Bug 修复 | 修复导致崩溃或数据丢失的问题 | ✅ 询问 |
| 累积多次更改 | 同一插件在会话中被修改 >= 3 次 | ✅ 询问 |
| 小优化 | 代码清理、格式符号处理 | ❌ 不询问 |
| 文档更新 | 只改 README、注释 | ❌ 不询问 |
如果用户确认发布Agent 需要更新所有版本相关的文件代码、README、docs 等)。
**发布时需要完成**
1.**更新版本号** - 修改插件文档字符串中的 `version` 字段
2.**中英文版本同步** - 确保两个版本的版本号一致
```python
"""
title: My Plugin
version: 0.2.0 # <- 必须更新这里!
version: 0.2.0 # <- 发布时更新这里!
...
"""
```
@@ -984,3 +1006,83 @@ Follow the [Conventional Commits](https://www.conventionalcommits.org/) specific
**Bad:**
- `新增导出PDF插件` (Chinese is not allowed)
- `update code` (Too vague)
---
## 🤖 Git Operations (Agent Rules)
**重要规则 (CRITICAL RULES FOR AI AGENTS)**:
AI Agent如 Copilot、Gemini、Claude 等)在执行 Git 操作时必须遵守以下规则:
| 操作 (Operation) | 允许 (Allowed) | 说明 (Description) |
|-----------------|---------------|---------------------|
| 创建功能分支 | ✅ 允许 | `git checkout -b feature/xxx` |
| 推送到功能分支 | ✅ 允许 | `git push origin feature/xxx` |
| 直接推送到 main | ❌ 禁止 | `git push origin main` 需要用户手动执行 |
| 合并到 main | ❌ 禁止 | 任何合并操作需要用户明确批准 |
| Rebase 到 main | ❌ 禁止 | 任何 rebase 操作需要用户明确批准 |
**规则详解 (Rule Details)**:
1. **Feature Branches Allowed**: Agent **可以**创建新的功能分支并推送到远程仓库
2. **No Direct Push to Main**: Agent **禁止**直接推送任何更改到 `main` 分支
3. **No Auto-Merge**: Agent **禁止**在未经用户明确批准的情况下合并任何分支到 `main`
4. **User Approval Required**: 任何影响 `main` 分支的操作push、merge、rebase都需要用户明确批准
> [!CAUTION]
> 违反上述规则可能导致代码库不稳定或触发意外的 CI/CD 流程。Agent 应始终在功能分支上工作,并让用户决定何时合并到主分支。
---
## ⏳ 长时间运行任务通知 (Long-running Task Notifications)
如果一个前台任务Foreground Task的运行时间预计超过 **3秒**,必须实现用户通知机制,以避免用户感到困惑。
**要求 (Requirements):**
1. **初始通知 (Initial Notification)**: 任务开始时**立即**发送第一条通知,告知用户正在处理中(例如:“正在使用 AI 生成中...”)。
2. **周期性通知 (Periodic Notification)**: 之后每隔 **5秒** 发送一次通知,告知用户任务仍在运行中。
3. **完成清理 (Cleanup)**: 任务完成后,应自动取消通知任务。
**代码示例 (Code Example):**
```python
import asyncio
async def long_running_task_with_notification(self, event_emitter, ...):
# 定义实际任务
async def actual_task():
# ... 执行耗时操作 ...
return result
# 定义通知任务
async def notification_task():
# 立即发送首次通知
if event_emitter:
await self._send_notification(event_emitter, "info", "正在使用 AI 生成中...")
# 之后每5秒通知一次
while True:
await asyncio.sleep(5)
if event_emitter:
await self._send_notification(event_emitter, "info", "仍在处理中,请耐心等待...")
# 并发运行任务
task_future = asyncio.ensure_future(actual_task())
notify_future = asyncio.ensure_future(notification_task())
# 等待任务完成
done, pending = await asyncio.wait(
[task_future, notify_future],
return_when=asyncio.FIRST_COMPLETED
)
# 取消通知任务
if not notify_future.done():
notify_future.cancel()
# 获取结果
if task_future in done:
return task_future.result()
```

View File

@@ -341,7 +341,13 @@ jobs:
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
# Check if there are any .py files to upload
if [ -d release_plugins ] && [ -n "$(find release_plugins -type f -name '*.py' 2>/dev/null)" ]; then
echo "Uploading plugin files..."
find release_plugins -type f -name "*.py" -print0 | xargs -0 gh release upload ${{ steps.version.outputs.version }} --clobber
else
echo "No plugin files to upload. Skipping asset upload."
fi
- name: Summary
run: |

View File

@@ -1,20 +1,21 @@
# Export to Excel
<span class="category-badge action">Action</span>
<span class="version-badge">v0.3.4</span>
<span class="version-badge">v0.3.6</span>
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
- **Smart Filename Generation**: Now supports generating filenames based on Chat Title, AI Summary, or Markdown Headers.
- **Configuration Options**: Added `TITLE_SOURCE` setting to control filename generation strategy.
### What's New in v0.3.6
- **OpenWebUI-Style Theme**: Modern dark header with light gray zebra striping for better readability.
- **Zebra Striping**: Alternating row colors for improved visual scanning.
- **Smart Data Type Conversion**: Automatically converts columns to numeric or datetime types.
- **Full Cell Bold/Italic**: Supports Markdown bold/italic formatting in Excel.
- **Partial Markdown Cleanup**: Removes partial Markdown symbols for cleaner output.
- **Export Scope**: Choose between "Last Message" or "All Messages".
- **Smart Sheet Naming**: Names sheets based on Markdown headers or message index.
- **Smart Filename Generation**: Generates filenames based on Chat Title, AI Summary, or Markdown Headers.
- **AI Title Generation**: Supports using a specific model (`MODEL_ID`) for title generation with progress notifications.
---

View File

@@ -1,20 +1,21 @@
# Export to Excel导出到 Excel
<span class="category-badge action">Action</span>
<span class="version-badge">v0.3.4</span>
<span class="version-badge">v0.3.6</span>
将聊天记录导出为 Excel 表格,便于分析、归档和分享。
### v0.3.5 更新内容
- **导出范围**: 新增 `EXPORT_SCOPE` 配置项,可选择导出“最后一条消息”(默认)或“所有消息”中的表格
- **智能 Sheet 命名**: 根据 Markdown 标题、AI 标题(如启用)或消息索引(如 `消息1-表1`)自动命名 Sheet
- **多表格支持**: 优化了对单条或多条消息中包含多个表格的处理
### v0.3.4 更新内容
- **智能文件名生成**支持根据对话标题、AI 总结或 Markdown 标题生成文件名
- **配置选项**:新增 `TITLE_SOURCE` 设置,用于控制文件名生成策略
### v0.3.6 更新内容
- **OpenWebUI 风格主题**:现代深灰表头,搭配浅灰斑马纹,提升可读性
- **斑马纹效果**:隔行变色,方便视觉扫描
- **智能数据类型转换**:自动将列转换为数字或日期类型
- **全单元格粗体/斜体**:支持 Markdown 粗体/斜体格式。
- **部分 Markdown 清理**:移除部分 Markdown 符号,输出更整洁。
- **导出范围**:可选择导出"最后一条消息"或"所有消息"。
- **智能 Sheet 命名**:根据 Markdown 标题或消息索引命名 Sheet
- **智能文件名生成**支持对话标题、AI 总结或 Markdown 标题生成文件名
- **AI 标题生成**:支持指定模型 (`MODEL_ID`) 生成标题,并提供生成进度通知。
---

View File

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

View File

@@ -53,7 +53,7 @@ Actions 是交互式插件,能够:
将聊天记录导出为 Excel 电子表格,方便分析或归档。
**版本:** 0.3.4
**版本:** 0.3.6
[:octicons-arrow-right-24: 查看文档](export-to-excel.md)

View File

@@ -2,15 +2,19 @@
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
## What's New in v0.3.6
- **OpenWebUI-Style Theme**: Modern dark header (#1f2937) with light gray zebra striping for better readability.
- **Zebra Striping**: Alternating row colors (#ffffff / #f3f4f6) for improved visual scanning.
- **Smart Data Type Conversion**: Automatically converts columns to numeric or datetime types with fallback to string.
- **Full Cell Bold/Italic**: Supports full cell Markdown bold (`**text**`) and italic (`*text*`) formatting in Excel.
- **Partial Markdown Cleanup**: Automatically removes partial Markdown formatting symbols (e.g., `Some **bold** text``Some bold text`) for cleaner Excel output.
- **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
- **Smart Filename Generation**: Now supports generating filenames based on Chat Title, AI Summary, or Markdown Headers.
- **Smart Filename Generation**: Supports generating filenames based on Chat Title, AI Summary, or Markdown Headers.
- **Configuration Options**: Added `TITLE_SOURCE` setting to control filename generation strategy.
- **AI Title Generation**: Added `MODEL_ID` setting to specify the model for AI title generation, with progress notifications.
## Features

View File

@@ -2,19 +2,23 @@
此插件允许你直接从聊天界面将对话历史导出为 Excel (.xlsx) 文件。
### v0.3.5 更新内容
- **导出范围**: 新增 `EXPORT_SCOPE` 配置项,可选择导出“最后一条消息”(默认)或“所有消息”中的表格。
## v0.3.6 更新内容
- **OpenWebUI 风格主题**:现代深灰表头 (#1f2937),搭配浅灰斑马纹,提升可读性。
- **斑马纹效果**:隔行变色(#ffffff / #f3f4f6),方便视觉扫描。
- **智能数据类型转换**:自动将列转换为数字或日期类型,无法转换时保持字符串。
- **全单元格粗体/斜体**:支持 Excel 中的全单元格 Markdown 粗体 (`**text**`) 和斜体 (`*text*`) 格式。
- **部分 Markdown 清理**:自动移除部分 Markdown 格式符号(如 `部分**加粗**文本``部分加粗文本`),使 Excel 输出更整洁。
- **导出范围**: 新增 `EXPORT_SCOPE` 配置项,可选择导出"最后一条消息"(默认)或"所有消息"中的表格。
- **智能 Sheet 命名**: 根据 Markdown 标题、AI 标题(如启用)或消息索引(如 `消息1-表1`)自动命名 Sheet。
- **多表格支持**: 优化了对单条或多条消息中包含多个表格的处理。
## v0.3.4 更新内容
- **智能文件名生成**支持根据对话标题、AI 总结或 Markdown 标题生成文件名。
- **配置选项**:新增 `TITLE_SOURCE` 设置,用于控制文件名生成策略。
- **AI 标题生成**:新增 `MODEL_ID` 设置用于指定 AI 标题生成模型,并支持生成进度通知。
## 功能特点
- **一键导出**:在聊天界面添加导出为 Excel按钮。
- **一键导出**:在聊天界面添加"导出为 Excel"按钮。
- **自动表头提取**:智能识别聊天内容中的表格标题。
- **多表支持**:支持处理单次对话中的多个表格。
@@ -28,7 +32,7 @@
## 使用方法
1. 安装插件。
2. 在任意对话中,点击导出为 Excel按钮。
2. 在任意对话中,点击"导出为 Excel"按钮。
3. 文件将自动下载到你的设备。
## 作者

View File

@@ -3,9 +3,9 @@ title: Export to Excel
author: Fu-Jie
author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.3.5
version: 0.3.6
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
description: Exports the current chat history to an Excel (.xlsx) file, with automatic header extraction.
description: Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting.
"""
import os
@@ -20,20 +20,25 @@ from open_webui.models.chats import Chats
from open_webui.models.users import Users
from open_webui.utils.chat import generate_chat_completion
from pydantic import BaseModel, Field
from typing import Literal
app = FastAPI()
class Action:
class Valves(BaseModel):
TITLE_SOURCE: str = Field(
TITLE_SOURCE: Literal["chat_title", "ai_generated", "markdown_title"] = Field(
default="chat_title",
description="Title Source: 'chat_title' (Chat Title), 'ai_generated' (AI Generated), 'markdown_title' (Markdown Title)",
)
EXPORT_SCOPE: str = Field(
EXPORT_SCOPE: Literal["last_message", "all_messages"] = Field(
default="last_message",
description="Export Scope: 'last_message' (Last Message Only), 'all_messages' (All Messages)",
)
MODEL_ID: str = Field(
default="",
description="Model ID for AI title generation. Leave empty to use the current chat model.",
)
def __init__(self):
self.valves = self.Valves()
@@ -181,6 +186,16 @@ class Action:
seen_names[name] = True
final_sheet_names.append(name)
# Notify user about the number of tables found
table_count = len(all_tables)
if self.valves.EXPORT_SCOPE == "all_messages":
await self._send_notification(
__event_emitter__,
"info",
f"Found {table_count} table(s) in all messages.",
)
# Wait a moment for user to see the notification before download dialog
await asyncio.sleep(1.5)
# Generate Workbook Title (Filename)
# Use the title of the chat, or the first header of the first message with tables
title = ""
@@ -194,6 +209,24 @@ class Action:
or not self.valves.TITLE_SOURCE
):
title = chat_title
elif self.valves.TITLE_SOURCE == "ai_generated":
# Use AI to generate a title based on message content
if target_messages and __request__:
# Get content from the first message with tables
content_for_title = ""
for msg in target_messages:
msg_content = msg.get("content", "")
if msg_content:
content_for_title = msg_content
break
if content_for_title:
title = await self.generate_title_using_ai(
body,
content_for_title,
user_id,
__request__,
__event_emitter__,
)
elif self.valves.TITLE_SOURCE == "markdown_title":
# Try to find first header in the first message that has content
for msg in target_messages:
@@ -316,32 +349,93 @@ class Action:
)
async def generate_title_using_ai(
self, body: dict, content: str, user_id: str, request: Any
self,
body: dict,
content: str,
user_id: str,
request: Any,
event_emitter: Callable = None,
) -> str:
if not request:
return ""
try:
user_obj = Users.get_user_by_id(user_id)
model = body.get("model")
# Use configured MODEL_ID or fallback to current chat model
model = (
self.valves.MODEL_ID.strip()
if self.valves.MODEL_ID
else body.get("model")
)
payload = {
"model": model,
"messages": [
{
"role": "system",
"content": "You are a helpful assistant. Generate a short, concise title (max 10 words) for the following text. Do not use quotes. Only output the title.",
"content": "You are a helpful assistant. Generate a short, concise filename (max 10 words) for an Excel export based on the following content. Do not use quotes or file extensions. Avoid special characters that are invalid in filenames. Only output the filename.",
},
{"role": "user", "content": content[:2000]}, # Limit content length
],
"stream": False,
}
response = await generate_chat_completion(request, payload, user_obj)
if response and "choices" in response:
return response["choices"][0]["message"]["content"].strip()
# Define the generation task
async def generate_task():
return await generate_chat_completion(request, payload, user_obj)
# Define the notification task
async def notification_task():
# Send initial notification immediately
if event_emitter:
await self._send_notification(
event_emitter,
"info",
"AI is generating a filename for your Excel file...",
)
# Subsequent notifications every 5 seconds
while True:
await asyncio.sleep(5)
if event_emitter:
await self._send_notification(
event_emitter,
"info",
"Still generating filename, please be patient...",
)
# Run tasks concurrently
gen_future = asyncio.ensure_future(generate_task())
notify_future = asyncio.ensure_future(notification_task())
done, pending = await asyncio.wait(
[gen_future, notify_future], return_when=asyncio.FIRST_COMPLETED
)
# Cancel notification task if generation is done
if not notify_future.done():
notify_future.cancel()
# Get result
if gen_future in done:
response = gen_future.result()
if response and "choices" in response:
return response["choices"][0]["message"]["content"].strip()
else:
# Should not happen if return_when=FIRST_COMPLETED and we cancel notify
await gen_future
response = gen_future.result()
if response and "choices" in response:
return response["choices"][0]["message"]["content"].strip()
except Exception as e:
print(f"Error generating title: {e}")
if event_emitter:
await self._send_notification(
event_emitter,
"warning",
f"AI title generation failed, using default title. Error: {str(e)}",
)
return ""
@@ -681,24 +775,51 @@ class Action:
with pd.ExcelWriter(file_path, engine="xlsxwriter") as writer:
workbook = writer.book
# OpenWebUI-style theme colors
HEADER_BG = "#1f2937" # Dark gray (matches OpenWebUI sidebar)
HEADER_FG = "#ffffff" # White text
ROW_ODD_BG = "#ffffff" # White for odd rows
ROW_EVEN_BG = "#f3f4f6" # Light gray for even rows (zebra striping)
BORDER_COLOR = "#e5e7eb" # Light border
# Define header style - Center aligned
header_format = workbook.add_format(
{
"bold": True,
"font_size": 12,
"font_color": "white",
"bg_color": "#00abbd",
"font_size": 11,
"font_name": "Arial",
"font_color": HEADER_FG,
"bg_color": HEADER_BG,
"border": 1,
"border_color": BORDER_COLOR,
"align": "center",
"valign": "vcenter",
"text_wrap": True,
}
)
# Text cell style - Left aligned
# Text cell style - Left aligned (odd rows)
text_format = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
}
)
# Text cell style - Left aligned (even rows - zebra)
text_format_alt = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
@@ -707,14 +828,51 @@ class Action:
# Number cell style - Right aligned
number_format = workbook.add_format(
{"border": 1, "align": "right", "valign": "vcenter"}
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "right",
"valign": "vcenter",
}
)
number_format_alt = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "right",
"valign": "vcenter",
}
)
# Integer format - Right aligned
integer_format = workbook.add_format(
{
"num_format": "0",
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "right",
"valign": "vcenter",
}
)
integer_format_alt = workbook.add_format(
{
"num_format": "0",
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "right",
"valign": "vcenter",
}
@@ -724,7 +882,24 @@ class Action:
decimal_format = workbook.add_format(
{
"num_format": "0.00",
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "right",
"valign": "vcenter",
}
)
decimal_format_alt = workbook.add_format(
{
"num_format": "0.00",
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "right",
"valign": "vcenter",
}
@@ -733,7 +908,24 @@ class Action:
# Date format - Center aligned
date_format = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "center",
"valign": "vcenter",
"text_wrap": True,
}
)
date_format_alt = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "center",
"valign": "vcenter",
"text_wrap": True,
@@ -743,7 +935,23 @@ class Action:
# Sequence format - Center aligned
sequence_format = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "center",
"valign": "vcenter",
}
)
sequence_format_alt = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "center",
"valign": "vcenter",
}
@@ -752,7 +960,25 @@ class Action:
# Bold cell style (for full cell bolding)
text_bold_format = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
"bold": True,
}
)
text_bold_format_alt = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
@@ -763,7 +989,11 @@ class Action:
# Italic cell style (for full cell italics)
text_italic_format = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
@@ -771,6 +1001,48 @@ class Action:
}
)
text_italic_format_alt = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
"italic": True,
}
)
# Code cell style (for inline code with highlight background)
CODE_BG = "#f0f0f0" # Light gray background for code
text_code_format = workbook.add_format(
{
"font_name": "Consolas",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": CODE_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
}
)
text_code_format_alt = workbook.add_format(
{
"font_name": "Consolas",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": CODE_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
}
)
for i, table in enumerate(tables):
try:
table_data = table["data"]
@@ -812,12 +1084,18 @@ class Action:
print(f"DataFrame created with columns: {list(df.columns)}")
# Fix pandas FutureWarning
# Smart data type conversion using pandas infer_objects
for col in df.columns:
# Try numeric conversion first
try:
df[col] = pd.to_numeric(df[col])
except (ValueError, TypeError):
pass
# Try datetime conversion
try:
df[col] = pd.to_datetime(df[col], errors="raise")
except (ValueError, TypeError):
# Keep as string, use infer_objects for optimization
df[col] = df[col].infer_objects()
# Write data first (without header)
df.to_excel(
@@ -829,21 +1107,25 @@ class Action:
)
worksheet = writer.sheets[sheet_name]
# Apply enhanced formatting
# Apply enhanced formatting with zebra striping
formats = {
"header": header_format,
"text": [text_format, text_format_alt],
"number": [number_format, number_format_alt],
"integer": [integer_format, integer_format_alt],
"decimal": [decimal_format, decimal_format_alt],
"date": [date_format, date_format_alt],
"sequence": [sequence_format, sequence_format_alt],
"bold": [text_bold_format, text_bold_format_alt],
"italic": [text_italic_format, text_italic_format_alt],
"code": [text_code_format, text_code_format_alt],
}
self.apply_enhanced_formatting(
worksheet,
df,
headers,
workbook,
header_format,
text_format,
number_format,
integer_format,
decimal_format,
date_format,
sequence_format,
text_bold_format,
text_italic_format,
formats,
)
except Exception as e:
@@ -860,26 +1142,22 @@ class Action:
df,
headers,
workbook,
header_format,
text_format,
number_format,
integer_format,
decimal_format,
date_format,
sequence_format,
text_bold_format=None,
text_italic_format=None,
formats,
):
"""
Apply enhanced formatting
- Header: Center aligned
Apply enhanced formatting with zebra striping
- Header: Center aligned (dark background)
- Number: Right aligned
- Text: Left aligned
- Date: Center aligned
- Sequence: Center aligned
- Zebra striping: alternating row colors
- Supports full cell Markdown bold (**text**) and italic (*text*)
"""
try:
# Extract format from formats dict
header_format = formats["header"]
# 1. Write headers (Center aligned)
print(f"Writing headers with enhanced alignment: {headers}")
for col_idx, header in enumerate(headers):
@@ -903,62 +1181,97 @@ class Action:
else:
column_types[col_idx] = "text"
# 3. Write and format data
# 3. Write and format data with zebra striping
for row_idx, row in df.iterrows():
# Determine if odd or even row (0-indexed, so row 0 is odd visually as row 1)
is_alt_row = (
row_idx % 2 == 1
) # Even index = odd visual row, use alt format
for col_idx, value in enumerate(row):
content_type = column_types.get(col_idx, "text")
# Select format based on content type
# Select format based on content type and zebra striping
fmt_idx = 1 if is_alt_row else 0
if content_type == "number":
# Number - Right aligned
if pd.api.types.is_numeric_dtype(df.iloc[:, col_idx]):
if pd.api.types.is_integer_dtype(df.iloc[:, col_idx]):
current_format = integer_format
current_format = formats["integer"][fmt_idx]
else:
try:
numeric_value = float(value)
if numeric_value.is_integer():
current_format = integer_format
current_format = formats["integer"][fmt_idx]
value = int(numeric_value)
else:
current_format = decimal_format
current_format = formats["decimal"][fmt_idx]
except (ValueError, TypeError):
current_format = decimal_format
current_format = formats["decimal"][fmt_idx]
else:
current_format = number_format
current_format = formats["number"][fmt_idx]
elif content_type == "date":
# Date - Center aligned
current_format = date_format
current_format = formats["date"][fmt_idx]
elif content_type == "sequence":
# Sequence - Center aligned
current_format = sequence_format
current_format = formats["sequence"][fmt_idx]
else:
# Text - Left aligned
current_format = text_format
current_format = formats["text"][fmt_idx]
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())
# Check for full cell code (`text`)
match_code = 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
row_idx + 1,
col_idx,
clean_value,
formats["bold"][fmt_idx],
)
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
row_idx + 1,
col_idx,
clean_value,
formats["italic"][fmt_idx],
)
elif match_code:
# Extract content and apply code format (highlighted)
clean_value = match_code.group(1)
worksheet.write(
row_idx + 1,
col_idx,
clean_value,
formats["code"][fmt_idx],
)
else:
worksheet.write(row_idx + 1, col_idx, value, current_format)
# Remove partial markdown formatting symbols (can't render partial formatting in Excel)
# Remove bold markers **text** -> text
clean_value = re.sub(r"\*\*(.+?)\*\*", r"\1", value)
# Remove italic markers *text* -> text (but not inside **)
clean_value = re.sub(
r"(?<!\*)\*([^*]+)\*(?!\*)", r"\1", clean_value
)
# Remove code markers `text` -> text
clean_value = re.sub(r"`(.+?)`", r"\1", clean_value)
worksheet.write(
row_idx + 1, col_idx, clean_value, current_format
)
else:
worksheet.write(row_idx + 1, col_idx, value, current_format)

View File

@@ -3,9 +3,9 @@ title: 导出为 Excel
author: Fu-Jie
author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui
version: 0.3.5
version: 0.3.6
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
description: 将当前对话历史导出为 Excel (.xlsx) 文件,支持自动提取表头
description: 从聊天消息中提取表格并导出为 Excel (.xlsx) 文件,支持智能格式化
"""
import os
@@ -20,20 +20,25 @@ from open_webui.models.chats import Chats
from open_webui.models.users import Users
from open_webui.utils.chat import generate_chat_completion
from pydantic import BaseModel, Field
from typing import Literal
app = FastAPI()
class Action:
class Valves(BaseModel):
TITLE_SOURCE: str = Field(
TITLE_SOURCE: Literal["chat_title", "ai_generated", "markdown_title"] = Field(
default="chat_title",
description="标题来源: 'chat_title' (对话标题), 'ai_generated' (AI生成), 'markdown_title' (Markdown标题)",
)
EXPORT_SCOPE: str = Field(
EXPORT_SCOPE: Literal["last_message", "all_messages"] = Field(
default="last_message",
description="导出范围: 'last_message' (仅最后一条消息), 'all_messages' (所有消息)",
)
MODEL_ID: str = Field(
default="",
description="AI 标题生成模型 ID。留空则使用当前对话模型。",
)
def __init__(self):
self.valves = self.Valves()
@@ -172,6 +177,17 @@ class Action:
seen_names[name] = True
final_sheet_names.append(name)
# 通知用户提取到的表格数量
table_count = len(all_tables)
if self.valves.EXPORT_SCOPE == "all_messages":
await self._send_notification(
__event_emitter__,
"info",
f"从所有消息中提取到 {table_count} 个表格。",
)
# 等待片刻让用户看到通知,再触发下载
await asyncio.sleep(1.5)
# Generate Workbook Title (Filename)
title = ""
chat_id = self.extract_chat_id(body, None)
@@ -184,6 +200,24 @@ class Action:
or not self.valves.TITLE_SOURCE
):
title = chat_title
elif self.valves.TITLE_SOURCE == "ai_generated":
# 使用 AI 根据消息内容生成标题
if target_messages and __request__:
# 获取第一条有表格的消息内容
content_for_title = ""
for msg in target_messages:
msg_content = msg.get("content", "")
if msg_content:
content_for_title = msg_content
break
if content_for_title:
title = await self.generate_title_using_ai(
body,
content_for_title,
user_id,
__request__,
__event_emitter__,
)
elif self.valves.TITLE_SOURCE == "markdown_title":
for msg in target_messages:
extracted = self.extract_title(msg.get("content", ""))
@@ -304,32 +338,93 @@ class Action:
)
async def generate_title_using_ai(
self, body: dict, content: str, user_id: str, request: Any
self,
body: dict,
content: str,
user_id: str,
request: Any,
event_emitter: Callable = None,
) -> str:
if not request:
return ""
try:
user_obj = Users.get_user_by_id(user_id)
model = body.get("model")
# 使用配置的 MODEL_ID 或回退到当前对话模型
model = (
self.valves.MODEL_ID.strip()
if self.valves.MODEL_ID
else body.get("model")
)
payload = {
"model": model,
"messages": [
{
"role": "system",
"content": "你是一个乐于助人的助手。请为以下文本生成一个简短、简洁的标题最多10个字。不要使用引号。只输出标题",
"content": "你是一个乐于助人的助手。请根据以下内容为 Excel 导出文件生成一个简短、简洁的文件名最多10个字。不要使用引号或文件扩展名。避免使用文件名中无效的特殊字符。只输出文件名",
},
{"role": "user", "content": content[:2000]}, # 限制内容长度
],
"stream": False,
}
response = await generate_chat_completion(request, payload, user_obj)
if response and "choices" in response:
return response["choices"][0]["message"]["content"].strip()
# 定义生成任务
async def generate_task():
return await generate_chat_completion(request, payload, user_obj)
# 定义通知任务
async def notification_task():
# 立即发送首次通知
if event_emitter:
await self._send_notification(
event_emitter,
"info",
"AI 正在为您生成文件名,请稍候...",
)
# 之后每5秒通知一次
while True:
await asyncio.sleep(5)
if event_emitter:
await self._send_notification(
event_emitter,
"info",
"文件名生成中,请耐心等待...",
)
# 并发运行任务
gen_future = asyncio.ensure_future(generate_task())
notify_future = asyncio.ensure_future(notification_task())
done, pending = await asyncio.wait(
[gen_future, notify_future], return_when=asyncio.FIRST_COMPLETED
)
# 如果生成完成,取消通知任务
if not notify_future.done():
notify_future.cancel()
# 获取结果
if gen_future in done:
response = gen_future.result()
if response and "choices" in response:
return response["choices"][0]["message"]["content"].strip()
else:
# 理论上不会发生,因为是 FIRST_COMPLETED 且我们取消了 notify
await gen_future
response = gen_future.result()
if response and "choices" in response:
return response["choices"][0]["message"]["content"].strip()
except Exception as e:
print(f"生成标题时出错: {e}")
if event_emitter:
await self._send_notification(
event_emitter,
"warning",
f"AI 文件名生成失败,将使用默认名称。错误: {str(e)}",
)
return ""
@@ -686,25 +781,52 @@ class Action:
with pd.ExcelWriter(file_path, engine="xlsxwriter") as writer:
workbook = writer.book
# 定义表头样式 - 居中对齐(符合中国规范)
# OpenWebUI 风格主题配色
HEADER_BG = "#1f2937" # 深灰色 (匹配 OpenWebUI 侧边栏)
HEADER_FG = "#ffffff" # 白色文字
ROW_ODD_BG = "#ffffff" # 奇数行白色
ROW_EVEN_BG = "#f3f4f6" # 偶数行浅灰 (斑马纹)
BORDER_COLOR = "#e5e7eb" # 浅色边框
# 表头样式 - 居中对齐
header_format = workbook.add_format(
{
"bold": True,
"font_size": 12,
"font_color": "white",
"bg_color": "#00abbd",
"font_size": 11,
"font_name": "Arial",
"font_color": HEADER_FG,
"bg_color": HEADER_BG,
"border": 1,
"align": "center", # 表头居中
"border_color": BORDER_COLOR,
"align": "center",
"valign": "vcenter",
"text_wrap": True,
}
)
# 文本单元格样式 - 左对齐
# 文本单元格样式 - 左对齐 (奇数行)
text_format = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"align": "left", # 文本左对齐
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
}
)
# 文本单元格样式 - 左对齐 (偶数行 - 斑马纹)
text_format_alt = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
}
@@ -712,15 +834,52 @@ class Action:
# 数值单元格样式 - 右对齐
number_format = workbook.add_format(
{"border": 1, "align": "right", "valign": "vcenter"} # 数值右对齐
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "right",
"valign": "vcenter",
}
)
number_format_alt = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "right",
"valign": "vcenter",
}
)
# 整数格式 - 右对齐
integer_format = workbook.add_format(
{
"num_format": "0",
"font_name": "Arial",
"font_size": 10,
"border": 1,
"align": "right", # 整数右对齐
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "right",
"valign": "vcenter",
}
)
integer_format_alt = workbook.add_format(
{
"num_format": "0",
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "right",
"valign": "vcenter",
}
)
@@ -729,8 +888,25 @@ class Action:
decimal_format = workbook.add_format(
{
"num_format": "0.00",
"font_name": "Arial",
"font_size": 10,
"border": 1,
"align": "right", # 小数右对齐
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "right",
"valign": "vcenter",
}
)
decimal_format_alt = workbook.add_format(
{
"num_format": "0.00",
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "right",
"valign": "vcenter",
}
)
@@ -738,8 +914,25 @@ class Action:
# 日期格式 - 居中对齐
date_format = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"align": "center", # 日期居中对齐
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "center",
"valign": "vcenter",
"text_wrap": True,
}
)
date_format_alt = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "center",
"valign": "vcenter",
"text_wrap": True,
}
@@ -748,8 +941,24 @@ class Action:
# 序号格式 - 居中对齐
sequence_format = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"align": "center", # 序号居中对齐
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "center",
"valign": "vcenter",
}
)
sequence_format_alt = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "center",
"valign": "vcenter",
}
)
@@ -757,7 +966,25 @@ class Action:
# 粗体单元格样式 (用于全单元格加粗)
text_bold_format = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
"bold": True,
}
)
text_bold_format_alt = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
@@ -768,7 +995,11 @@ class Action:
# 斜体单元格样式 (用于全单元格斜体)
text_italic_format = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_ODD_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
@@ -776,6 +1007,48 @@ class Action:
}
)
text_italic_format_alt = workbook.add_format(
{
"font_name": "Arial",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": ROW_EVEN_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
"italic": True,
}
)
# 代码单元格样式 (用于行内代码高亮显示)
CODE_BG = "#f0f0f0" # 代码浅灰背景
text_code_format = workbook.add_format(
{
"font_name": "Consolas",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": CODE_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
}
)
text_code_format_alt = workbook.add_format(
{
"font_name": "Consolas",
"font_size": 10,
"border": 1,
"border_color": BORDER_COLOR,
"bg_color": CODE_BG,
"align": "left",
"valign": "vcenter",
"text_wrap": True,
}
)
for i, table in enumerate(tables):
try:
table_data = table["data"]
@@ -817,12 +1090,18 @@ class Action:
print(f"DataFrame created with columns: {list(df.columns)}")
# 修复pandas FutureWarning - 使用try-except替代errors='ignore'
# 智能数据类型转换
for col in df.columns:
# 先尝试数字转换
try:
df[col] = pd.to_numeric(df[col])
except (ValueError, TypeError):
pass
# 尝试日期转换
try:
df[col] = pd.to_datetime(df[col], errors="raise")
except (ValueError, TypeError):
# 保持为字符串,使用 infer_objects 优化
df[col] = df[col].infer_objects()
# 先写入数据(不包含表头)
df.to_excel(
@@ -834,21 +1113,25 @@ class Action:
)
worksheet = writer.sheets[sheet_name]
# 应用符合中国规范的格式化
# 应用符合中国规范的格式化 (带斑马纹)
formats = {
"header": header_format,
"text": [text_format, text_format_alt],
"number": [number_format, number_format_alt],
"integer": [integer_format, integer_format_alt],
"decimal": [decimal_format, decimal_format_alt],
"date": [date_format, date_format_alt],
"sequence": [sequence_format, sequence_format_alt],
"bold": [text_bold_format, text_bold_format_alt],
"italic": [text_italic_format, text_italic_format_alt],
"code": [text_code_format, text_code_format_alt],
}
self.apply_chinese_standard_formatting(
worksheet,
df,
headers,
workbook,
header_format,
text_format,
number_format,
integer_format,
decimal_format,
date_format,
sequence_format,
text_bold_format,
text_italic_format,
formats,
)
except Exception as e:
@@ -865,26 +1148,22 @@ class Action:
df,
headers,
workbook,
header_format,
text_format,
number_format,
integer_format,
decimal_format,
date_format,
sequence_format,
text_bold_format=None,
text_italic_format=None,
formats,
):
"""
应用符合中国官方表格规范的格式化
- 表头: 居中对齐
应用符合中国官方表格规范的格式化 (带斑马纹)
- 表头: 居中对齐 (深色背景)
- 数值: 右对齐
- 文本: 左对齐
- 日期: 居中对齐
- 序号: 居中对齐
- 斑马纹: 隔行变色
- 支持全单元格 Markdown 粗体 (**text**) 和斜体 (*text*)
"""
try:
# 从 formats 字典提取格式
header_format = formats["header"]
# 1. 写入表头(居中对齐)
print(f"Writing headers with Chinese standard alignment: {headers}")
for col_idx, header in enumerate(headers):
@@ -908,62 +1187,95 @@ class Action:
else:
column_types[col_idx] = "text"
# 3. 写入并格式化数据(根据类型使用不同对齐方式
# 3. 写入并格式化数据(带斑马纹
for row_idx, row in df.iterrows():
# 确定奇偶行 (0-indexed, 所以 row 0 视觉上是第 1 行)
is_alt_row = row_idx % 2 == 1 # 偶数索引 = 奇数行, 使用 alt 格式
for col_idx, value in enumerate(row):
content_type = column_types.get(col_idx, "text")
# 根据内容类型选择格式
# 根据内容类型和斑马纹选择格式
fmt_idx = 1 if is_alt_row else 0
if content_type == "number":
# 数值类型 - 右对齐
if pd.api.types.is_numeric_dtype(df.iloc[:, col_idx]):
if pd.api.types.is_integer_dtype(df.iloc[:, col_idx]):
current_format = integer_format
current_format = formats["integer"][fmt_idx]
else:
try:
numeric_value = float(value)
if numeric_value.is_integer():
current_format = integer_format
current_format = formats["integer"][fmt_idx]
value = int(numeric_value)
else:
current_format = decimal_format
current_format = formats["decimal"][fmt_idx]
except (ValueError, TypeError):
current_format = decimal_format
current_format = formats["decimal"][fmt_idx]
else:
current_format = number_format
current_format = formats["number"][fmt_idx]
elif content_type == "date":
# 日期类型 - 居中对齐
current_format = date_format
current_format = formats["date"][fmt_idx]
elif content_type == "sequence":
# 序号类型 - 居中对齐
current_format = sequence_format
current_format = formats["sequence"][fmt_idx]
else:
# 文本类型 - 左对齐
current_format = text_format
current_format = formats["text"][fmt_idx]
if content_type == "text" and isinstance(value, str):
# 检查是否全单元格加粗 (**text**)
match_bold = re.fullmatch(r"\*\*(.+)\*\*", value.strip())
# 检查是否全单元格斜体 (*text*)
match_italic = re.fullmatch(r"\*(.+)\*", value.strip())
# 检查是否全单元格代码 (`text`)
match_code = 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
row_idx + 1,
col_idx,
clean_value,
formats["bold"][fmt_idx],
)
elif match_italic:
# 提取内容并应用斜体格式
clean_value = match_italic.group(1)
worksheet.write(
row_idx + 1, col_idx, clean_value, text_italic_format
row_idx + 1,
col_idx,
clean_value,
formats["italic"][fmt_idx],
)
elif match_code:
# 提取内容并应用代码格式 (高亮显示)
clean_value = match_code.group(1)
worksheet.write(
row_idx + 1,
col_idx,
clean_value,
formats["code"][fmt_idx],
)
else:
worksheet.write(row_idx + 1, col_idx, value, current_format)
# 移除部分 Markdown 格式符号 (Excel 无法渲染部分格式)
# 移除粗体标记 **text** -> text
clean_value = re.sub(r"\*\*(.+?)\*\*", r"\1", value)
# 移除斜体标记 *text* -> text (但不影响 ** 内部的内容)
clean_value = re.sub(
r"(?<!\*)\*([^*]+)\*(?!\*)", r"\1", clean_value
)
# 移除代码标记 `text` -> text
clean_value = re.sub(r"`(.+?)`", r"\1", clean_value)
worksheet.write(
row_idx + 1, col_idx, clean_value, current_format
)
else:
worksheet.write(row_idx + 1, col_idx, value, current_format)