fix: resolve mkdocs build warnings and broken links

This commit is contained in:
fujie
2026-01-14 23:46:56 +08:00
parent ab0daba80d
commit 70a96d0754
12 changed files with 287 additions and 157 deletions

View File

@@ -7,10 +7,10 @@
## 📚 Table of Contents
1. [Quick Start](#1-quick-start)
2. [Core Concepts & SDK Details](#2-core-concepts--sdk-details)
2. [Core Concepts & SDK Details](#2-core-concepts-sdk-details)
3. [Deep Dive into Plugin Types](#3-deep-dive-into-plugin-types)
4. [Advanced Development Patterns](#4-advanced-development-patterns)
5. [Best Practices & Design Principles](#5-best-practices--design-principles)
5. [Best Practices & Design Principles](#5-best-practices-design-principles)
6. [Troubleshooting](#6-troubleshooting)
---
@@ -351,8 +351,7 @@ async def action(self, body, __event_call__, __metadata__, ...):
#### Reference Implementations
- `plugins/actions/js-render-poc/infographic_markdown.py` - AntV Infographic + Data URL
- `plugins/actions/js-render-poc/js_render_poc.py` - Basic proof of concept
- `plugins/actions/infographic/infographic.py` - Production-ready implementation using AntV + Data URL
---

View File

@@ -4,19 +4,19 @@
## 📚 目录
1. [插件开发快速入门](#1-插件开发快速入门)
2. [核心概念与 SDK 详解](#2-核心概念与-sdk-详解)
3. [插件类型深度解析](#3-插件类型深度解析)
* [Action (动作)](#31-action-动作)
* [Filter (过滤器)](#32-filter-过滤器)
* [Pipe (管道)](#33-pipe-管道)
4. [高级开发模式](#4-高级开发模式)
5. [最佳实践与设计原则](#5-最佳实践与设计原则)
6. [故障排查](#6-故障排查)
1. [插件开发快速入门](#1-quick-start)
2. [核心概念与 SDK 详解](#2-core-concepts-sdk-details)
3. [插件类型深度解析](#3-plugin-types)
* [Action (动作)](#31-action)
* [Filter (过滤器)](#32-filter)
* [Pipe (管道)](#33-pipe)
4. [高级开发模式](#4-advanced-patterns)
5. [最佳实践与设计原则](#5-best-practices)
6. [故障排查](#6-troubleshooting)
---
## 1. 插件开发快速入门
## 1. 插件开发快速入门 {: #1-quick-start }
### 1.1 什么是 OpenWebUI 插件?
@@ -64,7 +64,7 @@ class Action:
---
## 2. 核心概念与 SDK 详解
## 2. 核心概念与 SDK 详解 {: #2-core-concepts-sdk-details }
### 2.1 ⚠️ 重要:同步与异步
@@ -107,9 +107,9 @@ class Filter:
---
## 3. 插件类型深度解析
## 3. 插件类型深度解析 {: #3-plugin-types }
### 3.1 Action (动作)
### 3.1 Action (动作) {: #31-action }
**定位**:在消息下方添加按钮,用户点击触发。
@@ -134,7 +134,7 @@ async def action(self, body, __event_call__):
await __event_call__({"type": "execute", "data": {"code": js}})
```
### 3.2 Filter (过滤器)
### 3.2 Filter (过滤器) {: #32-filter }
**定位**:中间件,拦截并修改请求/响应。
@@ -155,7 +155,7 @@ async def inlet(self, body, __metadata__):
return body
```
### 3.3 Pipe (管道)
### 3.3 Pipe (管道) {: #33-pipe }
**定位**:自定义模型/代理。
@@ -177,7 +177,7 @@ class Pipe:
---
## 4. 高级开发模式
## 4. 高级开发模式 {: #4-advanced-patterns }
### 4.1 Pipe 与 Filter 协同
利用 `__request__.app.state` 在不同插件间共享数据。
@@ -315,10 +315,9 @@ async def action(self, body, __event_call__, __metadata__, ...):
#### 参考实现
- `plugins/actions/js-render-poc/infographic_markdown.py` - AntV 信息图 + Data URL
- `plugins/actions/js-render-poc/js_render_poc.py` - 基础概念验证
- `plugins/actions/infographic/infographic.py` - 基于 AntV + Data URL 的生产级实现
## 5. 最佳实践与设计原则
## 5. 最佳实践与设计原则 {: #5-best-practices }
### 5.1 命名与定位
* **简短有力**:如 "闪记卡", "精读"。避免 "文本分析助手" 这种泛词。
@@ -344,7 +343,7 @@ except Exception as e:
---
## 6. 故障排查
## 6. 故障排查 {: #6-troubleshooting }
* **HTML 不显示?** 确保包裹在 ` ```html ... ``` ` 代码块中。
* **数据库报错?** 检查是否在 `async` 函数中直接调用了同步的 DB 方法,请使用 `asyncio.to_thread`

View File

@@ -73,13 +73,13 @@ hide:
[:octicons-arrow-right-24: Learn More](plugins/actions/smart-mind-map.md)
- :material-card-text:{ .lg .middle } **Knowledge Card**
- :material-card-text:{ .lg .middle } **Flash Card**
---
Quickly generates beautiful learning memory cards, perfect for studying and quick memorization.
Quickly generates beautiful flashcards from text, extracting key points and categories.
[:octicons-arrow-right-24: Learn More](plugins/actions/knowledge-card.md)
[:octicons-arrow-right-24: Learn More](plugins/actions/flash-card.md)
- :material-arrow-collapse-vertical:{ .lg .middle } **Async Context Compression**

View File

@@ -73,13 +73,13 @@ hide:
[:octicons-arrow-right-24: 了解更多](plugins/actions/smart-mind-map.md)
- :material-card-text:{ .lg .middle } **知识卡片**
- :material-card-text:{ .lg .middle } **Flash Card闪记卡**
---
快速生成精美的学习记忆卡片,非常适合学习和快速记忆。
[:octicons-arrow-right-24: 了解更多](plugins/actions/knowledge-card.md)
[:octicons-arrow-right-24: 了解更多](plugins/actions/flash-card.md)
- :material-arrow-collapse-vertical:{ .lg .middle } **异步上下文压缩**

View File

@@ -37,15 +37,15 @@ Actions 是交互式插件,能够:
[:octicons-arrow-right-24: 查看文档](smart-infographic.md)
- :material-card-text:{ .lg .middle } **Knowledge Card**
- :material-card-text:{ .lg .middle } **Flash Card闪记卡**
---
快速生成精美的学习记忆卡片,适合学习记忆。
快速生成精美的学习记忆卡片,非常适合学习和快速记忆。
**版本:** 0.2.2
**版本:** 0.2.4
[:octicons-arrow-right-24: 查看文档](knowledge-card.md)
[:octicons-arrow-right-24: 查看文档](flash-card.md)
- :material-file-excel:{ .lg .middle } **Export to Excel**
@@ -77,15 +77,7 @@ Actions 是交互式插件,能够:
[:octicons-arrow-right-24: 查看文档](deep-dive.zh.md)
- :material-image-text:{ .lg .middle } **信息图转 Markdown**
---
AI 驱动的信息图生成器,渲染 SVG 并以 Markdown Data URL 图片嵌入。
**版本:** 1.0.0
[:octicons-arrow-right-24: 查看文档](infographic-markdown.zh.md)
</div>

View File

@@ -36,15 +36,7 @@ Filter 充当消息管线中的中间件:
[:octicons-arrow-right-24: 查看文档](context-enhancement.md)
- :material-google:{ .lg .middle } **Gemini Manifold Companion**
---
Gemini Manifold Pipe 插件的伴随过滤器。
**版本:** 1.7.0
[:octicons-arrow-right-24: 查看文档](gemini-manifold-companion.md)
- :material-format-paint:{ .lg .middle } **Markdown Normalizer**

View File

@@ -48,15 +48,15 @@ OpenWebUI supports four types of plugins, each serving a different purpose:
| Plugin | Type | Description | Version |
|--------|------|-------------|---------|
| [Smart Mind Map](actions/smart-mind-map.md) | Action | Generate interactive mind maps from text | 0.8.0 |
| [Smart Infographic](actions/smart-infographic.md) | Action | Transform text into professional infographics | 1.0.0 |
| [Knowledge Card](actions/knowledge-card.md) | Action | Create beautiful learning flashcards | 0.2.0 |
| [Export to Excel](actions/export-to-excel.md) | Action | Export chat history to Excel files | 1.0.0 |
| [Export to Word](actions/export-to-word.md) | Action | Export chat content to Word (.docx) with formatting | 0.1.0 |
| [Async Context Compression](filters/async-context-compression.md) | Filter | Intelligent context compression | 1.0.0 |
| [Context Enhancement](filters/context-enhancement.md) | Filter | Enhance chat context | 1.0.0 |
| [Gemini Manifold Companion](filters/gemini-manifold-companion.md) | Filter | Companion for Gemini Manifold | 1.0.0 |
| [Gemini Manifold](pipes/gemini-manifold.md) | Pipe | Gemini model integration | 1.0.0 |
| [Smart Mind Map](actions/smart-mind-map.md) | Action | Generate interactive mind maps from text | 0.9.1 |
| [Smart Infographic](actions/smart-infographic.md) | Action | Transform text into professional infographics | 1.4.9 |
| [Flash Card](actions/flash-card.md) | Action | Create beautiful learning flashcards | 0.2.4 |
| [Export to Excel](actions/export-to-excel.md) | Action | Export chat history to Excel files | 0.3.7 |
| [Export to Word](actions/export-to-word.md) | Action | Export chat content to Word (.docx) with formatting | 0.4.3 |
| [Async Context Compression](filters/async-context-compression.md) | Filter | Intelligent context compression | 1.1.3 |
| [Context Enhancement](filters/context-enhancement.md) | Filter | Enhance chat context | 0.3.0 |
| [Multi-Model Context Merger](filters/multi-model-context-merger.md) | Filter | Merge context from multiple models | 0.1.0 |
| [Web Gemini Multimodal Filter](filters/web-gemini-multimodel.md) | Filter | Multimodal capabilities for any model | 0.3.2 |
| [MoE Prompt Refiner](pipelines/moe-prompt-refiner.md) | Pipeline | Multi-model prompt refinement | 1.0.0 |
---

View File

@@ -48,15 +48,15 @@ OpenWebUI 支持四种类型的插件,每种都有不同的用途:
| 插件 | 类型 | 描述 | 版本 |
|--------|------|-------------|---------|
| [Smart Mind Map智能思维导图](actions/smart-mind-map.md) | Action | 从文本生成交互式思维导图 | 0.8.0 |
| [Smart Infographic智能信息图](actions/smart-infographic.md) | Action | 将文本转成专业信息图 | 1.0.0 |
| [Knowledge Card知识卡片](actions/knowledge-card.md) | Action | 生成精美学习卡片 | 0.2.0 |
| [Export to Excel导出到 Excel](actions/export-to-excel.md) | Action | 导出聊天记录为 Excel | 1.0.0 |
| [Export to Word导出为 Word](actions/export-to-word.md) | Action | 将聊天内容导出为 Word (.docx) 并保留格式 | 0.1.0 |
| [Async Context Compression异步上下文压缩](filters/async-context-compression.md) | Filter | 智能上下文压缩 | 1.0.0 |
| [Context Enhancement上下文增强](filters/context-enhancement.md) | Filter | 提升对话上下文 | 1.0.0 |
| [Gemini Manifold Companion](filters/gemini-manifold-companion.md) | Filter | Gemini Manifold 伴侣 | 1.0.0 |
| [Gemini Manifold](pipes/gemini-manifold.md) | Pipe | Gemini 模型集成 | 1.0.0 |
| [Smart Mind Map智能思维导图](actions/smart-mind-map.md) | Action | 从文本生成交互式思维导图 | 0.9.1 |
| [Smart Infographic智能信息图](actions/smart-infographic.md) | Action | 将文本转成专业信息图 | 1.4.9 |
| [Flash Card闪记卡](actions/flash-card.md) | Action | 生成精美学习卡片 | 0.2.4 |
| [Export to Excel导出到 Excel](actions/export-to-excel.md) | Action | 导出聊天记录为 Excel | 0.3.7 |
| [Export to Word导出为 Word](actions/export-to-word.md) | Action | 将聊天内容导出为 Word (.docx) 并保留格式 | 0.4.3 |
| [Async Context Compression异步上下文压缩](filters/async-context-compression.md) | Filter | 智能上下文压缩 | 1.1.3 |
| [Context Enhancement上下文增强](filters/context-enhancement.md) | Filter | 提升对话上下文 | 0.3.0 |
| [Multi-Model Context Merger多模型上下文合并](filters/multi-model-context-merger.md) | Filter | 合并多个模型的上下文 | 0.1.0 |
| [Web Gemini Multimodal FilterWeb Gemini 多模态过滤器)](filters/web-gemini-multimodel.md) | Filter | 为任何模型提供多模态能力 | 0.3.2 |
| [MoE Prompt Refiner](pipelines/moe-prompt-refiner.md) | Pipeline | 多模型提示词优化 | 1.0.0 |
---

View File

@@ -15,19 +15,7 @@ Pipes 可以用于:
## 可用的 Pipe 插件
<div class="grid cards" markdown>
- :material-google:{ .lg .middle } **Gemini Manifold**
---
面向 Google Gemini 的集成流水线,支持完整流式返回。
**版本:** 1.0.0
[:octicons-arrow-right-24: 查看文档](gemini-manifold.md)
</div>
---

View File

@@ -97,14 +97,14 @@ plugins:
Documentation Guide: 文档编写指南
Smart Mind Map: 智能思维导图
Smart Infographic: 智能信息图
Knowledge Card: 知识卡片
Flash Card: 闪记卡
Export to Excel: 导出到 Excel
Export to Word: 导出为 Word
Summary: 摘要
Async Context Compression: 异步上下文压缩
Context Enhancement: 上下文增强
Gemini Manifold Companion: Gemini Manifold 伴侣
Gemini Manifold: Gemini Manifold
Multi-Model Context Merger: 多模型上下文合并
Web Gemini Multimodal Filter: Web Gemini 多模态过滤器
MoE Prompt Refiner: MoE 提示词优化器
- minify:
minify_html: true
@@ -184,17 +184,17 @@ nav:
- plugins/actions/index.md
- Smart Mind Map: plugins/actions/smart-mind-map.md
- Smart Infographic: plugins/actions/smart-infographic.md
- Knowledge Card: plugins/actions/knowledge-card.md
- Flash Card: plugins/actions/flash-card.md
- Export to Excel: plugins/actions/export-to-excel.md
- Export to Word: plugins/actions/export-to-word.md
- Filters:
- plugins/filters/index.md
- Async Context Compression: plugins/filters/async-context-compression.md
- Context Enhancement: plugins/filters/context-enhancement.md
- Gemini Manifold Companion: plugins/filters/gemini-manifold-companion.md
- Multi-Model Context Merger: plugins/filters/multi-model-context-merger.md
- Web Gemini Multimodal Filter: plugins/filters/web-gemini-multimodel.md
- Pipes:
- plugins/pipes/index.md
- Gemini Manifold: plugins/pipes/gemini-manifold.md
- Pipelines:
- plugins/pipelines/index.md
- MoE Prompt Refiner: plugins/pipelines/moe-prompt-refiner.md

View File

@@ -1,8 +1,8 @@
"""
title: Export to Excel
author: Fu-Jie
author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui
author_url: https://github.com/Fu-Jie/awesome-openwebui
funding_url: https://github.com/open-webui
version: 0.3.7
openwebui_id: 244b8f9d-7459-47d6-84d3-c7ae8e3ec710
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
@@ -32,6 +32,10 @@ class Action:
default="chat_title",
description="Title Source: 'chat_title' (Chat Title), 'ai_generated' (AI Generated), 'markdown_title' (Markdown Title)",
)
SHOW_STATUS: bool = Field(
default=True,
description="Whether to show operation status updates.",
)
EXPORT_SCOPE: Literal["last_message", "all_messages"] = Field(
default="last_message",
description="Export Scope: 'last_message' (Last Message Only), 'all_messages' (All Messages)",
@@ -40,14 +44,57 @@ class Action:
default="",
description="Model ID for AI title generation. Leave empty to use the current chat model.",
)
SHOW_DEBUG_LOG: bool = Field(
default=False,
description="Whether to print debug logs in the browser console.",
)
def __init__(self):
self.valves = self.Valves()
async def _send_notification(self, emitter: Callable, type: str, content: str):
await emitter(
{"type": "notification", "data": {"type": type, "content": content}}
)
async def _emit_status(
self,
emitter: Optional[Callable[[Any], Awaitable[None]]],
description: str,
done: bool = False,
):
"""Emits a status update event."""
if self.valves.SHOW_STATUS and emitter:
await emitter(
{"type": "status", "data": {"description": description, "done": done}}
)
async def _emit_notification(
self,
emitter: Optional[Callable[[Any], Awaitable[None]]],
content: str,
ntype: str = "info",
):
"""Emits a notification event (info, success, warning, error)."""
if emitter:
await emitter(
{"type": "notification", "data": {"type": ntype, "content": content}}
)
async def _emit_debug_log(self, emitter, title: str, data: dict):
"""Print structured debug logs in the browser console"""
if not self.valves.SHOW_DEBUG_LOG or not emitter:
return
try:
import json
js_code = f"""
(async function() {{
console.group("🛠️ {title}");
console.log({json.dumps(data, ensure_ascii=False)});
console.groupEnd();
}})();
"""
await emitter({"type": "execute", "data": {"code": js_code}})
except Exception as e:
print(f"Error emitting debug log: {e}")
async def action(
self,
@@ -190,17 +237,18 @@ class Action:
# Notify user about the number of tables found
table_count = len(all_tables)
if self.valves.EXPORT_SCOPE == "all_messages":
await self._send_notification(
await self._emit_notification(
__event_emitter__,
"info",
f"Found {table_count} table(s) in all messages.",
"info",
)
# 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 = ""
chat_id = self.extract_chat_id(body, None)
chat_ctx = self._get_chat_context(body, None)
chat_id = chat_ctx["chat_id"]
chat_title = ""
if chat_id:
chat_title = await self.fetch_chat_title(chat_id, user_id)
@@ -330,8 +378,8 @@ class Action:
},
}
)
await self._send_notification(
__event_emitter__, "error", "No tables found to export!"
await self._emit_notification(
__event_emitter__, "No tables found to export!", "error"
)
raise e
except Exception as e:
@@ -345,8 +393,8 @@ class Action:
},
}
)
await self._send_notification(
__event_emitter__, "error", "No tables found to export!"
await self._emit_notification(
__event_emitter__, "No tables found to export!", "error"
)
async def generate_title_using_ai(
@@ -389,20 +437,20 @@ class Action:
async def notification_task():
# Send initial notification immediately
if event_emitter:
await self._send_notification(
await self._emit_notification(
event_emitter,
"info",
"AI is generating a filename for your Excel file...",
"info",
)
# Subsequent notifications every 5 seconds
while True:
await asyncio.sleep(5)
if event_emitter:
await self._send_notification(
await self._emit_notification(
event_emitter,
"info",
"Still generating filename, please be patient...",
"info",
)
# Run tasks concurrently
@@ -432,10 +480,10 @@ class Action:
except Exception as e:
print(f"Error generating title: {e}")
if event_emitter:
await self._send_notification(
await self._emit_notification(
event_emitter,
"warning",
f"AI title generation failed, using default title. Error: {str(e)}",
"warning",
)
return ""
@@ -450,24 +498,56 @@ class Action:
return match.group(1).strip()
return ""
def extract_chat_id(self, body: dict, metadata: Optional[dict]) -> str:
"""Extract chat_id from body or metadata"""
if isinstance(body, dict):
chat_id = body.get("chat_id") or body.get("id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
"""Safely extracts user context information."""
if isinstance(__user__, (list, tuple)):
user_data = __user__[0] if __user__ else {}
elif isinstance(__user__, dict):
user_data = __user__
else:
user_data = {}
for key in ("chat", "conversation"):
nested = body.get(key)
if isinstance(nested, dict):
nested_id = nested.get("id") or nested.get("chat_id")
if isinstance(nested_id, str) and nested_id.strip():
return nested_id.strip()
if isinstance(metadata, dict):
chat_id = metadata.get("chat_id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
return ""
return {
"user_id": user_data.get("id", "unknown_user"),
"user_name": user_data.get("name", "User"),
"user_language": user_data.get("language", "en-US"),
}
def _get_chat_context(
self, body: dict, __metadata__: Optional[dict] = None
) -> Dict[str, str]:
"""
Unified extraction of chat context information (chat_id, message_id).
Prioritizes extraction from body, then metadata.
"""
chat_id = ""
message_id = ""
# 1. Try to get from body
if isinstance(body, dict):
chat_id = body.get("chat_id", "")
message_id = body.get("id", "") # message_id is usually 'id' in body
# Check body.metadata as fallback
if not chat_id or not message_id:
body_metadata = body.get("metadata", {})
if isinstance(body_metadata, dict):
if not chat_id:
chat_id = body_metadata.get("chat_id", "")
if not message_id:
message_id = body_metadata.get("message_id", "")
# 2. Try to get from __metadata__ (as supplement)
if __metadata__ and isinstance(__metadata__, dict):
if not chat_id:
chat_id = __metadata__.get("chat_id", "")
if not message_id:
message_id = __metadata__.get("message_id", "")
return {
"chat_id": str(chat_id).strip(),
"message_id": str(message_id).strip(),
}
async def fetch_chat_title(self, chat_id: str, user_id: str = "") -> str:
"""Fetch chat title from database by chat_id"""

View File

@@ -1,8 +1,8 @@
"""
title: 导出为 Excel
author: Fu-Jie
author_url: https://github.com/Fu-Jie
funding_url: https://github.com/Fu-Jie/awesome-openwebui
author_url: https://github.com/Fu-Jie/awesome-openwebui
funding_url: https://github.com/open-webui
version: 0.3.7
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xNSAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWN1oiLz48cGF0aCBkPSJNMTQgMnY0YTIgMiAwIDAgMCAyIDJoNCIvPjxwYXRoIGQ9Ik04IDEzaDIiLz48cGF0aCBkPSJNMTQgMTNoMiIvPjxwYXRoIGQ9Ik04IDE3aDIiLz48cGF0aCBkPSJNMTQgMTdoMiIvPjwvc3ZnPg==
description: 从聊天消息中提取表格并导出为 Excel (.xlsx) 文件,支持智能格式化。
@@ -31,6 +31,10 @@ class Action:
default="chat_title",
description="标题来源: 'chat_title' (对话标题), 'ai_generated' (AI生成), 'markdown_title' (Markdown标题)",
)
SHOW_STATUS: bool = Field(
default=True,
description="是否显示操作状态更新。",
)
EXPORT_SCOPE: Literal["last_message", "all_messages"] = Field(
default="last_message",
description="导出范围: 'last_message' (仅最后一条消息), 'all_messages' (所有消息)",
@@ -39,14 +43,57 @@ class Action:
default="",
description="AI 标题生成模型 ID。留空则使用当前对话模型。",
)
SHOW_DEBUG_LOG: bool = Field(
default=False,
description="是否在浏览器控制台打印调试日志。",
)
def __init__(self):
self.valves = self.Valves()
async def _send_notification(self, emitter: Callable, type: str, content: str):
await emitter(
{"type": "notification", "data": {"type": type, "content": content}}
)
async def _emit_status(
self,
emitter: Optional[Callable[[Any], Awaitable[None]]],
description: str,
done: bool = False,
):
"""Emits a status update event."""
if self.valves.SHOW_STATUS and emitter:
await emitter(
{"type": "status", "data": {"description": description, "done": done}}
)
async def _emit_notification(
self,
emitter: Optional[Callable[[Any], Awaitable[None]]],
content: str,
ntype: str = "info",
):
"""Emits a notification event (info, success, warning, error)."""
if emitter:
await emitter(
{"type": "notification", "data": {"type": ntype, "content": content}}
)
async def _emit_debug_log(self, emitter, title: str, data: dict):
"""在浏览器控制台打印结构化调试日志"""
if not self.valves.SHOW_DEBUG_LOG or not emitter:
return
try:
import json
js_code = f"""
(async function() {{
console.group("🛠️ {title}");
console.log({json.dumps(data, ensure_ascii=False)});
console.groupEnd();
}})();
"""
await emitter({"type": "execute", "data": {"code": js_code}})
except Exception as e:
print(f"Error emitting debug log: {e}")
async def action(
self,
@@ -180,17 +227,18 @@ class Action:
# 通知用户提取到的表格数量
table_count = len(all_tables)
if self.valves.EXPORT_SCOPE == "all_messages":
await self._send_notification(
await self._emit_notification(
__event_emitter__,
"info",
f"从所有消息中提取到 {table_count} 个表格。",
"info",
)
# 等待片刻让用户看到通知,再触发下载
await asyncio.sleep(1.5)
# Generate Workbook Title (Filename)
title = ""
chat_id = self.extract_chat_id(body, None)
chat_ctx = self._get_chat_context(body, None)
chat_id = chat_ctx["chat_id"]
chat_title = ""
if chat_id:
chat_title = await self.fetch_chat_title(chat_id, user_id)
@@ -318,8 +366,8 @@ class Action:
},
}
)
await self._send_notification(
__event_emitter__, "error", "未找到可导出的表格!"
await self._emit_notification(
__event_emitter__, "未找到可导出的表格!", "error"
)
raise e
except Exception as e:
@@ -333,8 +381,8 @@ class Action:
},
}
)
await self._send_notification(
__event_emitter__, "error", "未找到可导出的表格!"
await self._emit_notification(
__event_emitter__, "未找到可导出的表格!", "error"
)
async def generate_title_using_ai(
@@ -377,20 +425,20 @@ class Action:
async def notification_task():
# 立即发送首次通知
if event_emitter:
await self._send_notification(
await self._emit_notification(
event_emitter,
"info",
"AI 正在为您生成文件名,请稍候...",
"info",
)
# 之后每5秒通知一次
while True:
await asyncio.sleep(5)
if event_emitter:
await self._send_notification(
await self._emit_notification(
event_emitter,
"info",
"文件名生成中,请耐心等待...",
"info",
)
# 并发运行任务
@@ -420,10 +468,10 @@ class Action:
except Exception as e:
print(f"生成标题时出错: {e}")
if event_emitter:
await self._send_notification(
await self._emit_notification(
event_emitter,
"warning",
f"AI 文件名生成失败,将使用默认名称。错误: {str(e)}",
"warning",
)
return ""
@@ -438,24 +486,56 @@ class Action:
return match.group(1).strip()
return ""
def extract_chat_id(self, body: dict, metadata: Optional[dict]) -> str:
"""从 body 或 metadata 中提取 chat_id"""
if isinstance(body, dict):
chat_id = body.get("chat_id") or body.get("id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
"""安全提取用户上下文信息。"""
if isinstance(__user__, (list, tuple)):
user_data = __user__[0] if __user__ else {}
elif isinstance(__user__, dict):
user_data = __user__
else:
user_data = {}
for key in ("chat", "conversation"):
nested = body.get(key)
if isinstance(nested, dict):
nested_id = nested.get("id") or nested.get("chat_id")
if isinstance(nested_id, str) and nested_id.strip():
return nested_id.strip()
if isinstance(metadata, dict):
chat_id = metadata.get("chat_id")
if isinstance(chat_id, str) and chat_id.strip():
return chat_id.strip()
return ""
return {
"user_id": user_data.get("id", "unknown_user"),
"user_name": user_data.get("name", "用户"),
"user_language": user_data.get("language", "zh-CN"),
}
def _get_chat_context(
self, body: dict, __metadata__: Optional[dict] = None
) -> Dict[str, str]:
"""
统一提取聊天上下文信息 (chat_id, message_id)。
优先从 body 中提取,其次从 metadata 中提取。
"""
chat_id = ""
message_id = ""
# 1. 尝试从 body 获取
if isinstance(body, dict):
chat_id = body.get("chat_id", "")
message_id = body.get("id", "") # message_id 在 body 中通常是 id
# 再次检查 body.metadata
if not chat_id or not message_id:
body_metadata = body.get("metadata", {})
if isinstance(body_metadata, dict):
if not chat_id:
chat_id = body_metadata.get("chat_id", "")
if not message_id:
message_id = body_metadata.get("message_id", "")
# 2. 尝试从 __metadata__ 获取 (作为补充)
if __metadata__ and isinstance(__metadata__, dict):
if not chat_id:
chat_id = __metadata__.get("chat_id", "")
if not message_id:
message_id = __metadata__.get("message_id", "")
return {
"chat_id": str(chat_id).strip(),
"message_id": str(message_id).strip(),
}
async def fetch_chat_title(self, chat_id: str, user_id: str = "") -> str:
"""通过 chat_id 从数据库获取对话标题"""