feat: 添加信息图插件,并更新相关插件模板和开发文档。
This commit is contained in:
@@ -1627,6 +1627,18 @@ More info about the header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Re
|
||||
|
||||
In the Tools definition metadata you can specify custom packages. When you click `Save` the line will be parsed and `pip install` will be run on all requirements at once.
|
||||
|
||||
:::warning
|
||||
|
||||
**🚨 CRITICAL WARNING: Potential for Package Version Conflicts**
|
||||
|
||||
When multiple tools define different versions of the same package (e.g., Tool A requires `pandas==1.5.0` and Tool B requires `pandas==2.0.0`), Open WebUI installs them in a non-deterministic order. This can lead to unpredictable behavior and break one or more of your tools.
|
||||
|
||||
**The only robust solution to this problem is to use an OpenAPI tool server.**
|
||||
|
||||
We strongly recommend using an [OpenAPI tool server](/features/plugin/tools/openapi-servers/) to avoid these dependency conflicts.
|
||||
|
||||
:::
|
||||
|
||||
Keep in mind that as pip is used in the same process as Open WebUI, the UI will be completely unresponsive during the installation.
|
||||
|
||||
No measures are taken to handle package conflicts with Open WebUI's requirements. That means that specifying requirements can break Open WebUI if you're not careful. You might be able to work around this by specifying `open-webui` itself as a requirement.
|
||||
|
||||
@@ -117,6 +117,10 @@ class Action:
|
||||
default=False,
|
||||
description="Whether to force clear previous plugin results (if True, overwrites instead of merging).",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.",
|
||||
)
|
||||
# Add other configuration fields as needed
|
||||
# MAX_TEXT_LENGTH: int = Field(default=2000, description="...")
|
||||
|
||||
@@ -175,6 +179,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""Extract text from message content, supporting multimodal message formats."""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# Multimodal message: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
@@ -278,10 +297,30 @@ class Action:
|
||||
|
||||
# 2. Input Validation
|
||||
messages = body.get("messages", [])
|
||||
if not messages or not messages[-1].get("content"):
|
||||
if not messages:
|
||||
return body # Or handle error
|
||||
|
||||
original_content = messages[-1]["content"]
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"User"
|
||||
if role == "user"
|
||||
else "Assistant" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} Message {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
return body # Or handle error
|
||||
|
||||
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
if len(original_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
warning_msg = f"Text too short ({len(original_content)} chars). Minimum required: {self.valves.MIN_TEXT_LENGTH}."
|
||||
|
||||
@@ -117,6 +117,10 @@ class Action:
|
||||
default=False,
|
||||
description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="用于生成的最近消息数量。设置为1仅使用最后一条消息,更大值可包含更多上下文。",
|
||||
)
|
||||
# 根据需要添加其他配置字段
|
||||
# MAX_TEXT_LENGTH: int = Field(default=2000, description="...")
|
||||
|
||||
@@ -186,6 +190,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""从消息内容中提取文本,支持多模态消息格式。"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# 多模态消息: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
@@ -289,10 +308,30 @@ class Action:
|
||||
|
||||
# 2. 输入验证
|
||||
messages = body.get("messages", [])
|
||||
if not messages or not messages[-1].get("content"):
|
||||
if not messages:
|
||||
return body # 或者处理错误
|
||||
|
||||
original_content = messages[-1]["content"]
|
||||
# 根据 MESSAGE_COUNT 获取最近 N 条消息
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# 聚合选中消息的内容,带标签
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"用户"
|
||||
if role == "user"
|
||||
else "助手" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} 消息 {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
return body # 或者处理错误
|
||||
|
||||
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
if len(original_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
warning_msg = f"文本过短 ({len(original_content)} 字符)。最少需要: {self.valves.MIN_TEXT_LENGTH}。"
|
||||
|
||||
67
plugins/actions/infographic/README.md
Normal file
67
plugins/actions/infographic/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# 信息图 - OpenWebUI Action 插件
|
||||
|
||||
将文本内容智能转换为美观的信息图,基于蚂蚁集团 AntV Infographic 引擎。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🤖 **AI 驱动**: 使用 LLM 自动分析文本内容并生成信息图语法
|
||||
- 📊 **多种模板**: 支持列表、流程、层级等多种信息图类型
|
||||
- 🎨 **自动图标**: 使用 `ref:search` 语法自动匹配高质量图标
|
||||
- 💾 **多格式导出**: 支持下载 SVG、PNG 和独立 HTML 文件
|
||||
- 🎯 **零配置**: 开箱即用,无需额外设置
|
||||
|
||||
## 安装
|
||||
|
||||
1. 将 `信息图.py` 文件复制到 Open WebUI 的插件目录:
|
||||
```
|
||||
plugins/actions/infographic/
|
||||
```
|
||||
|
||||
2. 重启 Open WebUI 或在管理界面重新加载插件
|
||||
|
||||
3. 在聊天界面的 Action 菜单中即可看到 "信息图" 选项
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 在聊天框输入需要可视化的文本内容(建议 100 字符以上)
|
||||
2. 点击 "信息图" Action 按钮
|
||||
3. AI 将自动分析文本并生成信息图
|
||||
4. 可以下载 SVG、PNG 或 HTML 格式的文件
|
||||
|
||||
### 示例文本
|
||||
|
||||
```
|
||||
我们的产品开发流程包括三个主要阶段:
|
||||
1. 需求分析 - 收集和分析用户需求,确定产品方向
|
||||
2. 设计开发 - 完成 UI/UX 设计和前后端开发
|
||||
3. 测试上线 - 进行质量验证并正式发布
|
||||
```
|
||||
|
||||
## 配置选项(Valves)
|
||||
|
||||
- **SHOW_STATUS**: 是否显示操作状态更新(默认: True)
|
||||
- **MODEL_ID**: 用于分析的 LLM 模型 ID(默认: 使用当前对话模型)
|
||||
- **MIN_TEXT_LENGTH**: 最小文本长度要求(默认: 100 字符)
|
||||
- **CLEAR_PREVIOUS_HTML**: 是否清除之前的插件输出(默认: False)
|
||||
|
||||
## 支持的信息图类型
|
||||
|
||||
插件会根据文本内容自动选择最合适的模板:
|
||||
|
||||
- **列表型**: `list-row-horizontal-icon-arrow`, `list-grid`
|
||||
- **层级型**: `tree-vertical`, `tree-horizontal`
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端**: Python, OpenWebUI Action API
|
||||
- **前端**: AntV Infographic (CDN)
|
||||
- **AI**: 自定义提示词工程
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 致谢
|
||||
|
||||
- [AntV Infographic](https://infographic.antv.vision/) - 信息图渲染引擎
|
||||
- [Open WebUI](https://github.com/open-webui/open-webui) - AI 聊天界面
|
||||
43
plugins/actions/infographic/README_CN.md
Normal file
43
plugins/actions/infographic/README_CN.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# AntV Infographic 智能信息图插件
|
||||
|
||||
将文本内容一键转换为精美的信息图。支持列表、层级、流程、关系、对比、分析、图表等多种可视化形式。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **智能分析**: 自动识别文本结构,选择最合适的图表模板。
|
||||
- **丰富模板**: 支持 20+ 种 AntV 信息图模板,涵盖列表、树图、思维导图、流程图、桑基图、SWOT、象限图、柱状图、饼图等。
|
||||
- **自动配图**: 智能搜索并匹配合适的图标。
|
||||
- **多格式导出**: 支持导出为 SVG, PNG, HTML 格式。
|
||||
- **多语言支持**: 输出语言跟随用户设定。
|
||||
|
||||
## 使用方法
|
||||
|
||||
在 Open WebUI 聊天框中,直接输入文本或上传文档,然后启用该插件。插件会自动分析内容并生成信息图。
|
||||
|
||||
### 支持的图表类型
|
||||
|
||||
#### 1. 列表与层级
|
||||
- **列表**: 网格卡片 (`list-grid`), 垂直列表 (`list-vertical`)
|
||||
- **树图**: 垂直树 (`tree-vertical`), 水平树 (`tree-horizontal`)
|
||||
- **思维导图**: `mindmap`
|
||||
|
||||
#### 2. 顺序与关系
|
||||
- **流程**: 路线图 (`sequence-roadmap`), 之字形流程 (`sequence-zigzag`), 水平流程 (`sequence-horizontal`)
|
||||
- **关系**: 桑基图 (`relation-sankey`), 循环关系 (`relation-circle`)
|
||||
|
||||
#### 3. 对比与分析
|
||||
- **对比**: 二元对比 (`compare-binary`), 对比表 (`compare-table`)
|
||||
- **分析**: SWOT 分析 (`compare-swot`), 象限图 (`quadrant-quarter`)
|
||||
|
||||
#### 4. 图表与数据
|
||||
- **统计**: 统计卡片 (`statistic-card`)
|
||||
- **图表**: 柱状图 (`chart-bar`), 条形图 (`chart-column`), 折线图 (`chart-line`), 饼图 (`chart-pie`), 环形图 (`chart-doughnut`)
|
||||
|
||||
## 安装
|
||||
|
||||
将 `infographic.py` (英文版) 或 `信息图.py` (中文版) 放入 Open WebUI 的插件目录即可。
|
||||
|
||||
## 依赖
|
||||
|
||||
- 插件依赖 `@antv/infographic` 库 (通过 CDN 加载)。
|
||||
- 需要联网权限以加载 CDN 资源和图标。
|
||||
43
plugins/actions/infographic/README_EN.md
Normal file
43
plugins/actions/infographic/README_EN.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# AntV Infographic Plugin
|
||||
|
||||
Transform text content into beautiful infographics with a single click. Supports lists, hierarchies, processes, relationships, comparisons, analysis, charts, and more.
|
||||
|
||||
## Features
|
||||
|
||||
- **Smart Analysis**: Automatically identifies text structure and selects the best template.
|
||||
- **Rich Templates**: Supports 20+ AntV infographic templates, including lists, trees, mind maps, roadmaps, Sankey diagrams, SWOT, quadrant charts, bar charts, pie charts, etc.
|
||||
- **Auto Icons**: Intelligently searches and matches appropriate icons.
|
||||
- **Multi-format Export**: Export as SVG, PNG, or HTML.
|
||||
- **Multi-language**: Output language follows user settings.
|
||||
|
||||
## Usage
|
||||
|
||||
In the Open WebUI chat interface, simply input text or upload a document, then enable this plugin. The plugin will analyze the content and generate an infographic.
|
||||
|
||||
### Supported Chart Types
|
||||
|
||||
#### 1. List & Hierarchy
|
||||
- **List**: Grid Cards (`list-grid`), Vertical List (`list-vertical`)
|
||||
- **Tree**: Vertical Tree (`tree-vertical`), Horizontal Tree (`tree-horizontal`)
|
||||
- **Mindmap**: `mindmap`
|
||||
|
||||
#### 2. Sequence & Relationship
|
||||
- **Process**: Roadmap (`sequence-roadmap`), Zigzag Process (`sequence-zigzag`), Horizontal Process (`sequence-horizontal`)
|
||||
- **Relationship**: Sankey Diagram (`relation-sankey`), Circular Relationship (`relation-circle`)
|
||||
|
||||
#### 3. Comparison & Analysis
|
||||
- **Comparison**: Binary Comparison (`compare-binary`), Comparison Table (`compare-table`)
|
||||
- **Analysis**: SWOT Analysis (`compare-swot`), Quadrant Chart (`quadrant-quarter`)
|
||||
|
||||
#### 4. Charts & Data
|
||||
- **Statistics**: Statistic Cards (`statistic-card`)
|
||||
- **Charts**: Bar Chart (`chart-bar`), Column Chart (`chart-column`), Line Chart (`chart-line`), Pie Chart (`chart-pie`), Doughnut Chart (`chart-doughnut`)
|
||||
|
||||
## Installation
|
||||
|
||||
Place `infographic.py` (English version) or `信息图.py` (Chinese version) into your Open WebUI plugins directory.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Depends on `@antv/infographic` library (loaded via CDN).
|
||||
- Requires internet access to load CDN resources and icons.
|
||||
277
plugins/actions/infographic/debug_card_spacing.html
Normal file
277
plugins/actions/infographic/debug_card_spacing.html
Normal file
@@ -0,0 +1,277 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>卡片间距调试工具</title>
|
||||
<script src="https://registry.npmmirror.com/@antv/infographic/0.2.1/files/dist/infographic.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f0f0f0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.debug-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 320px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.controls h2 {
|
||||
margin-top: 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.controls label {
|
||||
display: block;
|
||||
margin: 16px 0 6px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.controls input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-top: 16px;
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.controls button:hover {
|
||||
background: #4f46e5;
|
||||
}
|
||||
|
||||
.controls .info {
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.preview {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#infographic-container {
|
||||
min-height: 500px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* 基础样式 */
|
||||
#infographic-container svg text {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important;
|
||||
}
|
||||
|
||||
#infographic-container svg foreignObject {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
|
||||
/* 主标题样式 */
|
||||
#infographic-container svg foreignObject[data-element-type="title"]>* {
|
||||
font-size: 1.2em !important;
|
||||
font-weight: bold !important;
|
||||
line-height: 1.4 !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
/* 页面副标题样式 */
|
||||
#infographic-container svg foreignObject[data-element-type="desc"]>* {
|
||||
font-size: 0.6em !important;
|
||||
line-height: 1.4 !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
/* 卡片标题和描述样式(这里是重点调试区域) */
|
||||
#infographic-container svg foreignObject[data-element-type="item-label"]>* {
|
||||
font-size: 0.6em !important;
|
||||
line-height: 1.4 !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
#infographic-container svg foreignObject[data-element-type="item-desc"]>* {
|
||||
line-height: 1.4 !important;
|
||||
white-space: normal !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="debug-container">
|
||||
<div class="controls">
|
||||
<h2>🔧 卡片间距调试面板</h2>
|
||||
|
||||
<label>卡片标题与描述间距 (margin-top)</label>
|
||||
<input type="text" id="label-desc-gap" value="8px" placeholder="例如: 8px, 12px, 1em">
|
||||
|
||||
<label>卡片标题字体大小</label>
|
||||
<input type="text" id="label-size" value="0.6em" placeholder="例如: 0.6em, 12px">
|
||||
|
||||
<label>卡片描述字体大小</label>
|
||||
<input type="text" id="desc-size" value="0.5em" placeholder="例如: 0.5em, 10px">
|
||||
|
||||
<label>卡片内边距 (padding)</label>
|
||||
<input type="text" id="card-padding" value="8px" placeholder="例如: 8px, 12px">
|
||||
|
||||
<button onclick="applyStyles()">应用样式</button>
|
||||
<button onclick="reRender()" style="background: #10b981; margin-top: 8px;">重新渲染</button>
|
||||
|
||||
<div class="info">
|
||||
<strong>说明:</strong><br>
|
||||
• 标题与描述间距:控制标题和内容之间的垂直间距<br>
|
||||
• 字体大小:分别控制标题和描述的文字大小<br>
|
||||
• 内边距:控制整个卡片内的留白
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview">
|
||||
<div id="infographic-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function applyStyles() {
|
||||
console.log('[Debug] 应用卡片样式...');
|
||||
|
||||
const labelDescGap = document.getElementById('label-desc-gap').value;
|
||||
const labelSize = document.getElementById('label-size').value;
|
||||
const descSize = document.getElementById('desc-size').value;
|
||||
const cardPadding = document.getElementById('card-padding').value;
|
||||
|
||||
const container = document.getElementById('infographic-container');
|
||||
|
||||
// 卡片标题
|
||||
const itemLabels = container.querySelectorAll('svg foreignObject[data-element-type="item-label"]');
|
||||
itemLabels.forEach(fo => {
|
||||
const firstChild = fo.querySelector(':scope > *');
|
||||
if (firstChild) {
|
||||
firstChild.style.setProperty('font-size', labelSize, 'important');
|
||||
firstChild.style.setProperty('margin-bottom', labelDescGap, 'important');
|
||||
}
|
||||
});
|
||||
|
||||
// 卡片描述
|
||||
const itemDescs = container.querySelectorAll('svg foreignObject[data-element-type="item-desc"]');
|
||||
itemDescs.forEach(fo => {
|
||||
const firstChild = fo.querySelector(':scope > *');
|
||||
if (firstChild) {
|
||||
firstChild.style.setProperty('font-size', descSize, 'important');
|
||||
}
|
||||
});
|
||||
|
||||
// 尝试调整卡片整体内边距(通过调整 g 元素的 padding)
|
||||
const cardGroups = container.querySelectorAll('svg g[data-element-type="items-group"] > g');
|
||||
cardGroups.forEach(group => {
|
||||
// 这里可能需要通过调整内部 g 的 transform 来实现
|
||||
// AntV 的布局比较复杂,padding 可能需要通过其他方式实现
|
||||
});
|
||||
|
||||
console.log('[Debug] 卡片样式已应用:', {
|
||||
labelDescGap,
|
||||
labelSize,
|
||||
descSize,
|
||||
cardPadding
|
||||
});
|
||||
}
|
||||
|
||||
function reRender() {
|
||||
const container = document.getElementById('infographic-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
const syntaxContent = `infographic list-grid-compact-card
|
||||
data
|
||||
title 🚀 功能亮点说明
|
||||
desc 系统核心逻辑与交互视觉特性
|
||||
items
|
||||
- label 核心流程演示
|
||||
icon mdi/database-cog
|
||||
desc 包含4个数据源、4条分流管道(运费/商品/退货)以及UNION ALL与GROUP BY聚合节点
|
||||
- label 灵动动画效果
|
||||
icon mdi/motion-play
|
||||
desc 支持5个订单多色并行流转、管道霓虹闪烁光晕、节点旋转及仪表盘数值跳动反馈
|
||||
- label 极简交互控制
|
||||
icon mdi/cursor-default-click
|
||||
desc 内置开始/暂停/重置逻辑,支持1x至4x倍速调整,并集成实时仪表板与日志系统
|
||||
- label 赛博朋克风格
|
||||
icon mdi/palette
|
||||
desc 采用深蓝紫色调、霓虹绿文字影、网格背景以及渐变发光边框,打造极客视觉体验`;
|
||||
|
||||
if (typeof AntVInfographic === 'undefined') {
|
||||
container.innerHTML = '<div style="color: red; padding: 20px;">⚠️ AntV Infographic 库未加载</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { Infographic } = AntVInfographic;
|
||||
const instance = new Infographic({
|
||||
container: '#infographic-container',
|
||||
padding: 24,
|
||||
});
|
||||
instance.render(syntaxContent);
|
||||
console.log('[Debug] 渲染完成');
|
||||
|
||||
// 渲染完成后自动应用样式
|
||||
setTimeout(() => {
|
||||
applyStyles();
|
||||
}, 300);
|
||||
} catch (error) {
|
||||
container.innerHTML = '<div style="color: red; padding: 20px;">⚠️ 渲染错误: ' + error.message + '</div>';
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载后自动渲染
|
||||
window.onload = function () {
|
||||
setTimeout(reRender, 500);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
342
plugins/actions/infographic/debug_styles.html
Normal file
342
plugins/actions/infographic/debug_styles.html
Normal file
@@ -0,0 +1,342 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>信息图样式调试</title>
|
||||
<script src="https://registry.npmmirror.com/@antv/infographic/0.2.1/files/dist/infographic.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f0f0f0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.debug-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.controls {
|
||||
width: 300px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.controls h2 {
|
||||
margin-top: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.controls label {
|
||||
display: block;
|
||||
margin: 12px 0 4px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.controls input,
|
||||
.controls select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.controls button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-top: 16px;
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.controls button:hover {
|
||||
background: #4f46e5;
|
||||
}
|
||||
|
||||
.preview {
|
||||
flex: 1;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#infographic-container {
|
||||
min-height: 500px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* ========== 可调试的样式 ========== */
|
||||
/* 这些样式可以直接在这里修改,刷新页面即可看到效果 */
|
||||
|
||||
/* 基础字体 */
|
||||
#infographic-container svg text {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important;
|
||||
}
|
||||
|
||||
#infographic-container svg foreignObject {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
|
||||
/* 标题样式 - 根据需要调整 */
|
||||
#infographic-container svg foreignObject h1,
|
||||
#infographic-container svg foreignObject [class*="title"] {
|
||||
font-size: 0.8em !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
#infographic-container svg foreignObject h2,
|
||||
#infographic-container svg foreignObject [class*="subtitle"] {
|
||||
font-size: 0.85em !important;
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
/* ========== 调试样式结束 ========== */
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="debug-container">
|
||||
<div class="controls">
|
||||
<h2>🔧 样式调试面板</h2>
|
||||
|
||||
<label>一级标题字体大小</label>
|
||||
<input type="text" id="h1-size" value="1.2em" placeholder="例如: 1.2em, 20px">
|
||||
|
||||
<label>二级标题字体大小</label>
|
||||
<input type="text" id="h2-size" value="0.6em" placeholder="例如: 0.6em, 12px">
|
||||
|
||||
<label>标题换行设置</label>
|
||||
<select id="title-wrap">
|
||||
<option value="nowrap">不换行 (nowrap)</option>
|
||||
<option value="normal">自动换行 (normal)</option>
|
||||
</select>
|
||||
|
||||
<label>行高</label>
|
||||
<input type="text" id="line-height" value="1.4" placeholder="例如: 1.4, 1.6">
|
||||
|
||||
<button onclick="applyStyles()">应用样式</button>
|
||||
<button onclick="reRender()" style="background: #10b981; margin-top: 8px;">重新渲染</button>
|
||||
|
||||
<hr style="margin: 20px 0; border: none; border-top: 1px solid #eee;">
|
||||
|
||||
<label>自定义语法内容</label>
|
||||
<textarea id="syntax-input" rows="10" style="width: 100%; font-size: 12px; font-family: monospace;">infographic list-grid
|
||||
data
|
||||
title 2025职业足球运动员成功要素解析
|
||||
desc 在高度竞争的现代足球体系下,从天赋到职业的进化路径
|
||||
items
|
||||
- label 核心入场券:天赋与身体
|
||||
desc 速度、爆发力及极佳的球感是底线。基本功必须化为肌肉记忆。
|
||||
icon mdi/lightning-bolt
|
||||
- label 决定上限:极度自律
|
||||
desc 包含严苛的饮食、睡眠管理及强大的抗压心态。
|
||||
icon mdi/shield-star
|
||||
- label 现代智慧:球商 (IQ)
|
||||
desc 复杂的战术理解力与阅读比赛能力。
|
||||
icon mdi/brain
|
||||
- label 隐形推手:机遇与平台
|
||||
desc 专业的青训体系、能发掘你的伯乐。
|
||||
icon mdi/star-shooting</textarea>
|
||||
</div>
|
||||
|
||||
<div class="preview">
|
||||
<div id="infographic-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function applyStyles() {
|
||||
console.log('[Debug] 应用样式到已渲染的元素...');
|
||||
|
||||
const h1Size = document.getElementById('h1-size').value;
|
||||
const h2Size = document.getElementById('h2-size').value;
|
||||
const titleWrap = document.getElementById('title-wrap').value;
|
||||
const lineHeight = document.getElementById('line-height').value;
|
||||
|
||||
const container = document.getElementById('infographic-container');
|
||||
|
||||
// 直接操作 SVG 内的所有文本元素
|
||||
const allTexts = container.querySelectorAll('svg text');
|
||||
allTexts.forEach(text => {
|
||||
text.style.setProperty('font-size', h1Size, 'important');
|
||||
});
|
||||
|
||||
// 操作 foreignObject 内的所有元素
|
||||
const foreignObjects = container.querySelectorAll('svg foreignObject');
|
||||
let mainTitleCount = 0;
|
||||
let itemLabelCount = 0;
|
||||
let itemDescCount = 0;
|
||||
let otherCount = 0;
|
||||
|
||||
foreignObjects.forEach(fo => {
|
||||
// 检查 foreignObject 自身的 data-element-type 属性
|
||||
const elementType = fo.getAttribute('data-element-type');
|
||||
|
||||
// 设置 foreignObject 本身的行高
|
||||
fo.style.setProperty('line-height', lineHeight, 'important');
|
||||
|
||||
// 获取内部的第一个元素(通常是 span)
|
||||
const firstChild = fo.querySelector(':scope > *');
|
||||
|
||||
if (!firstChild) return;
|
||||
|
||||
// 根据 foreignObject 的类型设置样式
|
||||
if (elementType === 'title') {
|
||||
// 主标题
|
||||
mainTitleCount++;
|
||||
firstChild.style.setProperty('font-size', h1Size, 'important');
|
||||
firstChild.style.setProperty('font-weight', 'bold', 'important');
|
||||
firstChild.style.setProperty('line-height', lineHeight, 'important');
|
||||
if (titleWrap === 'nowrap') {
|
||||
firstChild.style.setProperty('white-space', 'nowrap', 'important');
|
||||
firstChild.style.setProperty('overflow', 'hidden', 'important');
|
||||
firstChild.style.setProperty('text-overflow', 'ellipsis', 'important');
|
||||
} else {
|
||||
firstChild.style.setProperty('white-space', 'normal', 'important');
|
||||
firstChild.style.setProperty('overflow', 'visible', 'important');
|
||||
firstChild.style.setProperty('text-overflow', 'clip', 'important');
|
||||
}
|
||||
} else if (elementType === 'desc') {
|
||||
// 页面副标题(主标题下方的描述)
|
||||
itemLabelCount++; // 归入副标题计数
|
||||
firstChild.style.setProperty('font-size', h2Size, 'important');
|
||||
firstChild.style.setProperty('line-height', lineHeight, 'important');
|
||||
if (titleWrap === 'nowrap') {
|
||||
firstChild.style.setProperty('white-space', 'nowrap', 'important');
|
||||
firstChild.style.setProperty('overflow', 'hidden', 'important');
|
||||
firstChild.style.setProperty('text-overflow', 'ellipsis', 'important');
|
||||
} else {
|
||||
firstChild.style.setProperty('white-space', 'normal', 'important');
|
||||
firstChild.style.setProperty('overflow', 'visible', 'important');
|
||||
firstChild.style.setProperty('text-overflow', 'clip', 'important');
|
||||
}
|
||||
} else if (elementType === 'item-label') {
|
||||
// 卡片标题
|
||||
itemLabelCount++;
|
||||
firstChild.style.setProperty('font-size', h2Size, 'important');
|
||||
firstChild.style.setProperty('line-height', lineHeight, 'important');
|
||||
if (titleWrap === 'nowrap') {
|
||||
firstChild.style.setProperty('white-space', 'nowrap', 'important');
|
||||
firstChild.style.setProperty('overflow', 'hidden', 'important');
|
||||
firstChild.style.setProperty('text-overflow', 'ellipsis', 'important');
|
||||
} else {
|
||||
firstChild.style.setProperty('white-space', 'normal', 'important');
|
||||
firstChild.style.setProperty('overflow', 'visible', 'important');
|
||||
firstChild.style.setProperty('text-overflow', 'clip', 'important');
|
||||
}
|
||||
} else if (elementType === 'item-desc') {
|
||||
// 卡片描述
|
||||
itemDescCount++;
|
||||
firstChild.style.setProperty('line-height', lineHeight, 'important');
|
||||
firstChild.style.setProperty('white-space', 'normal', 'important');
|
||||
} else {
|
||||
// 其他元素
|
||||
otherCount++;
|
||||
firstChild.style.setProperty('line-height', lineHeight, 'important');
|
||||
firstChild.style.setProperty('white-space', 'normal', 'important');
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[Debug] foreignObject 处理:', '主标题:', mainTitleCount, '卡片标题:', itemLabelCount, '卡片描述:', itemDescCount, '其他:', otherCount);
|
||||
|
||||
console.log('[Debug] 样式已应用到', allTexts.length, '个文本元素和', foreignObjects.length, '个 foreignObject');
|
||||
}
|
||||
|
||||
function reRender() {
|
||||
const container = document.getElementById('infographic-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
let syntaxContent = document.getElementById('syntax-input').value.trim();
|
||||
|
||||
if (typeof AntVInfographic === 'undefined') {
|
||||
container.innerHTML = '<div style="color: red; padding: 20px;">⚠️ AntV Infographic 库未加载</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用模板映射(与插件保持一致)
|
||||
const TEMPLATE_MAPPING = {
|
||||
'list-grid': 'list-grid-compact-card',
|
||||
'list-vertical': 'list-column-simple-vertical-arrow',
|
||||
'tree-vertical': 'hierarchy-tree-tech-style-capsule-item',
|
||||
'tree-horizontal': 'hierarchy-tree-lr-tech-style-capsule-item',
|
||||
'mindmap': 'hierarchy-mindmap-branch-gradient-capsule-item',
|
||||
'sequence-roadmap': 'sequence-roadmap-vertical-simple',
|
||||
'sequence-zigzag': 'sequence-horizontal-zigzag-simple',
|
||||
'sequence-horizontal': 'sequence-horizontal-zigzag-simple',
|
||||
'relation-sankey': 'relation-sankey-simple',
|
||||
'relation-circle': 'relation-circle-icon-badge',
|
||||
'compare-binary': 'compare-binary-horizontal-simple-vs',
|
||||
'compare-swot': 'compare-swot',
|
||||
'compare-table': 'compare-table-simple',
|
||||
'quadrant-quarter': 'quadrant-quarter-simple-card',
|
||||
'statistic-card': 'list-grid-compact-card',
|
||||
'chart-bar': 'chart-bar-plain-text',
|
||||
'chart-column': 'chart-column-simple',
|
||||
'chart-line': 'chart-line-plain-text',
|
||||
'chart-area': 'chart-area-simple',
|
||||
'chart-pie': 'chart-pie-plain-text',
|
||||
'chart-doughnut': 'chart-pie-donut-plain-text'
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(TEMPLATE_MAPPING)) {
|
||||
const regex = new RegExp(`infographic\\s+${key}(?=\\s|$)`, 'i');
|
||||
if (regex.test(syntaxContent)) {
|
||||
console.log(`[Debug] 模板映射: ${key} -> ${value}`);
|
||||
syntaxContent = syntaxContent.replace(regex, `infographic ${value}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const { Infographic } = AntVInfographic;
|
||||
const instance = new Infographic({
|
||||
container: '#infographic-container',
|
||||
padding: 24,
|
||||
});
|
||||
instance.render(syntaxContent);
|
||||
console.log('[Debug] 渲染完成');
|
||||
|
||||
// 渲染完成后应用样式
|
||||
setTimeout(() => {
|
||||
applyStyles();
|
||||
}, 300);
|
||||
} catch (error) {
|
||||
container.innerHTML = '<div style="color: red; padding: 20px;">⚠️ 渲染错误: ' + error.message + '</div>';
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载后自动渲染
|
||||
window.onload = function () {
|
||||
setTimeout(reRender, 500);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
1098
plugins/actions/infographic/infographic.py
Normal file
1098
plugins/actions/infographic/infographic.py
Normal file
File diff suppressed because one or more lines are too long
194
plugins/actions/infographic/infographic_templates.json
Normal file
194
plugins/actions/infographic/infographic_templates.json
Normal file
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"templates": [
|
||||
"chart-bar-basic-bar",
|
||||
"chart-bar-basic-column",
|
||||
"chart-bar-grouped-bar",
|
||||
"chart-bar-grouped-column",
|
||||
"chart-bar-percent-stacked-bar",
|
||||
"chart-bar-percent-stacked-column",
|
||||
"chart-bar-stacked-bar",
|
||||
"chart-bar-stacked-column",
|
||||
"chart-line-basic-line",
|
||||
"chart-line-curved-line",
|
||||
"chart-line-multi-line",
|
||||
"chart-line-step-line",
|
||||
"chart-pie-basic-donut",
|
||||
"chart-pie-basic-pie",
|
||||
"chart-pie-compact-card",
|
||||
"chart-pie-donut-compact-card",
|
||||
"chart-wordcloud-basic",
|
||||
"compare-binary-circle-progress",
|
||||
"compare-binary-compact-card",
|
||||
"compare-binary-dashed-arrow-compact-card",
|
||||
"compare-binary-dashed-line-compact-card",
|
||||
"compare-binary-horizontal-icon-arrow",
|
||||
"compare-binary-simple-horizontal-arrow",
|
||||
"compare-binary-simple-vertical-arrow",
|
||||
"compare-binary-tech-style-compact-card",
|
||||
"compare-hierarchy-compact-card",
|
||||
"compare-hierarchy-dashed-arrow-compact-card",
|
||||
"compare-hierarchy-dashed-line-compact-card",
|
||||
"compare-hierarchy-horizontal-icon-arrow",
|
||||
"compare-hierarchy-simple-horizontal-arrow",
|
||||
"compare-hierarchy-simple-vertical-arrow",
|
||||
"compare-hierarchy-tech-style-compact-card",
|
||||
"compare-swot-compact-card",
|
||||
"compare-swot-dashed-arrow-compact-card",
|
||||
"compare-swot-dashed-line-compact-card",
|
||||
"compare-swot-horizontal-icon-arrow",
|
||||
"compare-swot-simple-horizontal-arrow",
|
||||
"compare-swot-simple-vertical-arrow",
|
||||
"compare-swot-tech-style-compact-card",
|
||||
"hierarchy-mindmap-branch-gradient-capsule-item",
|
||||
"hierarchy-mindmap-branch-gradient-circle-progress",
|
||||
"hierarchy-mindmap-branch-gradient-compact-card",
|
||||
"hierarchy-mindmap-branch-gradient-lined-palette",
|
||||
"hierarchy-mindmap-branch-gradient-rounded-rect",
|
||||
"hierarchy-mindmap-level-gradient-capsule-item",
|
||||
"hierarchy-mindmap-level-gradient-circle-progress",
|
||||
"hierarchy-mindmap-level-gradient-compact-card",
|
||||
"hierarchy-mindmap-level-gradient-lined-palette",
|
||||
"hierarchy-mindmap-level-gradient-rounded-rect",
|
||||
"hierarchy-tree-bt-curved-line-badge-card",
|
||||
"hierarchy-tree-bt-curved-line-capsule-item",
|
||||
"hierarchy-tree-bt-curved-line-compact-card",
|
||||
"hierarchy-tree-bt-curved-line-ribbon-card",
|
||||
"hierarchy-tree-bt-curved-line-rounded-rect-node",
|
||||
"hierarchy-tree-bt-dashed-arrow-badge-card",
|
||||
"hierarchy-tree-bt-dashed-arrow-capsule-item",
|
||||
"hierarchy-tree-bt-dashed-arrow-compact-card",
|
||||
"hierarchy-tree-bt-dashed-arrow-ribbon-card",
|
||||
"hierarchy-tree-bt-dashed-arrow-rounded-rect-node",
|
||||
"hierarchy-tree-bt-dashed-line-badge-card",
|
||||
"hierarchy-tree-bt-dashed-line-capsule-item",
|
||||
"hierarchy-tree-bt-dashed-line-compact-card",
|
||||
"hierarchy-tree-bt-dashed-line-ribbon-card",
|
||||
"hierarchy-tree-bt-dashed-line-rounded-rect-node",
|
||||
"hierarchy-tree-bt-distributed-origin-badge-card",
|
||||
"hierarchy-tree-bt-distributed-origin-capsule-item",
|
||||
"hierarchy-tree-bt-distributed-origin-compact-card",
|
||||
"hierarchy-tree-bt-distributed-origin-ribbon-card",
|
||||
"hierarchy-tree-bt-distributed-origin-rounded-rect-node",
|
||||
"hierarchy-tree-bt-tech-style-badge-card",
|
||||
"hierarchy-tree-bt-tech-style-capsule-item",
|
||||
"hierarchy-tree-bt-tech-style-compact-card",
|
||||
"hierarchy-tree-bt-tech-style-ribbon-card",
|
||||
"hierarchy-tree-bt-tech-style-rounded-rect-node",
|
||||
"hierarchy-tree-curved-line-badge-card",
|
||||
"hierarchy-tree-curved-line-capsule-item",
|
||||
"hierarchy-tree-curved-line-compact-card",
|
||||
"hierarchy-tree-curved-line-ribbon-card",
|
||||
"hierarchy-tree-curved-line-rounded-rect-node",
|
||||
"hierarchy-tree-dashed-arrow-badge-card",
|
||||
"hierarchy-tree-dashed-arrow-capsule-item",
|
||||
"hierarchy-tree-dashed-arrow-compact-card",
|
||||
"hierarchy-tree-dashed-arrow-ribbon-card",
|
||||
"hierarchy-tree-dashed-arrow-rounded-rect-node",
|
||||
"hierarchy-tree-dashed-line-badge-card",
|
||||
"hierarchy-tree-dashed-line-capsule-item",
|
||||
"hierarchy-tree-dashed-line-compact-card",
|
||||
"hierarchy-tree-dashed-line-ribbon-card",
|
||||
"hierarchy-tree-dashed-line-rounded-rect-node",
|
||||
"hierarchy-tree-distributed-origin-badge-card",
|
||||
"hierarchy-tree-distributed-origin-capsule-item",
|
||||
"hierarchy-tree-distributed-origin-compact-card",
|
||||
"hierarchy-tree-distributed-origin-ribbon-card",
|
||||
"hierarchy-tree-distributed-origin-rounded-rect-node",
|
||||
"hierarchy-tree-lr-curved-line-badge-card",
|
||||
"hierarchy-tree-lr-curved-line-capsule-item",
|
||||
"hierarchy-tree-lr-curved-line-compact-card",
|
||||
"hierarchy-tree-lr-curved-line-ribbon-card",
|
||||
"hierarchy-tree-lr-curved-line-rounded-rect-node",
|
||||
"hierarchy-tree-lr-dashed-arrow-badge-card",
|
||||
"hierarchy-tree-lr-dashed-arrow-capsule-item",
|
||||
"hierarchy-tree-lr-dashed-arrow-compact-card",
|
||||
"hierarchy-tree-lr-dashed-arrow-ribbon-card",
|
||||
"hierarchy-tree-lr-dashed-arrow-rounded-rect-node",
|
||||
"hierarchy-tree-lr-dashed-line-badge-card",
|
||||
"hierarchy-tree-lr-dashed-line-capsule-item",
|
||||
"hierarchy-tree-lr-dashed-line-compact-card",
|
||||
"hierarchy-tree-lr-dashed-line-ribbon-card",
|
||||
"hierarchy-tree-lr-dashed-line-rounded-rect-node",
|
||||
"hierarchy-tree-lr-distributed-origin-badge-card",
|
||||
"hierarchy-tree-lr-distributed-origin-capsule-item",
|
||||
"hierarchy-tree-lr-distributed-origin-compact-card",
|
||||
"hierarchy-tree-lr-distributed-origin-ribbon-card",
|
||||
"hierarchy-tree-lr-distributed-origin-rounded-rect-node",
|
||||
"hierarchy-tree-lr-tech-style-badge-card",
|
||||
"hierarchy-tree-lr-tech-style-capsule-item",
|
||||
"hierarchy-tree-lr-tech-style-compact-card",
|
||||
"hierarchy-tree-lr-tech-style-ribbon-card",
|
||||
"hierarchy-tree-lr-tech-style-rounded-rect-node",
|
||||
"hierarchy-tree-rl-curved-line-badge-card",
|
||||
"hierarchy-tree-rl-curved-line-capsule-item",
|
||||
"hierarchy-tree-rl-curved-line-compact-card",
|
||||
"hierarchy-tree-rl-curved-line-ribbon-card",
|
||||
"hierarchy-tree-rl-curved-line-rounded-rect-node",
|
||||
"hierarchy-tree-rl-dashed-arrow-badge-card",
|
||||
"hierarchy-tree-rl-dashed-arrow-capsule-item",
|
||||
"hierarchy-tree-rl-dashed-arrow-compact-card",
|
||||
"hierarchy-tree-rl-dashed-arrow-ribbon-card",
|
||||
"hierarchy-tree-rl-dashed-arrow-rounded-rect-node",
|
||||
"hierarchy-tree-rl-dashed-line-badge-card",
|
||||
"hierarchy-tree-rl-dashed-line-capsule-item",
|
||||
"hierarchy-tree-rl-dashed-line-compact-card",
|
||||
"hierarchy-tree-rl-dashed-line-ribbon-card",
|
||||
"hierarchy-tree-rl-dashed-line-rounded-rect-node",
|
||||
"hierarchy-tree-rl-distributed-origin-badge-card",
|
||||
"hierarchy-tree-rl-distributed-origin-capsule-item",
|
||||
"hierarchy-tree-rl-distributed-origin-compact-card",
|
||||
"hierarchy-tree-rl-distributed-origin-ribbon-card",
|
||||
"hierarchy-tree-rl-distributed-origin-rounded-rect-node",
|
||||
"hierarchy-tree-rl-tech-style-badge-card",
|
||||
"hierarchy-tree-rl-tech-style-capsule-item",
|
||||
"hierarchy-tree-rl-tech-style-compact-card",
|
||||
"hierarchy-tree-rl-tech-style-ribbon-card",
|
||||
"hierarchy-tree-rl-tech-style-rounded-rect-node",
|
||||
"hierarchy-tree-tech-style-badge-card",
|
||||
"hierarchy-tree-tech-style-capsule-item",
|
||||
"hierarchy-tree-tech-style-compact-card",
|
||||
"hierarchy-tree-tech-style-ribbon-card",
|
||||
"hierarchy-tree-tech-style-rounded-rect-node",
|
||||
"list-column-done-list",
|
||||
"list-column-simple-vertical-arrow",
|
||||
"list-column-vertical-icon-arrow",
|
||||
"list-grid-badge-card",
|
||||
"list-grid-candy-card-lite",
|
||||
"list-grid-circular-progress",
|
||||
"list-grid-compact-card",
|
||||
"list-grid-done-list",
|
||||
"list-grid-horizontal-icon-arrow",
|
||||
"list-grid-progress-card",
|
||||
"list-grid-ribbon-card",
|
||||
"list-grid-simple",
|
||||
"list-pyramid-badge-card",
|
||||
"list-pyramid-compact-card",
|
||||
"list-pyramid-rounded-rect-node",
|
||||
"list-row-circular-progress",
|
||||
"list-row-horizontal-icon-arrow",
|
||||
"list-row-horizontal-icon-line",
|
||||
"list-row-simple-horizontal-arrow",
|
||||
"list-row-simple-illus",
|
||||
"list-sector-half-plain-text",
|
||||
"list-sector-plain-text",
|
||||
"list-sector-simple",
|
||||
"quadrant-scatter-basic",
|
||||
"quadrant-scatter-compact-card",
|
||||
"quadrant-scatter-dashed-arrow-compact-card",
|
||||
"quadrant-scatter-dashed-line-compact-card",
|
||||
"quadrant-scatter-horizontal-icon-arrow",
|
||||
"quadrant-scatter-simple-horizontal-arrow",
|
||||
"quadrant-scatter-simple-vertical-arrow",
|
||||
"quadrant-scatter-tech-style-compact-card",
|
||||
"relation-graph-circular-layout",
|
||||
"relation-graph-force-directed",
|
||||
"sequence-circular-basic",
|
||||
"sequence-roadmap-vertical-simple",
|
||||
"sequence-snake-basic",
|
||||
"sequence-stairs-basic",
|
||||
"sequence-steps-basic",
|
||||
"sequence-timeline-basic",
|
||||
"sequence-timeline-done-list",
|
||||
"sequence-zigzag-basic"
|
||||
]
|
||||
}
|
||||
688
plugins/actions/infographic/test_infographic.html
Normal file
688
plugins/actions/infographic/test_infographic.html
Normal file
@@ -0,0 +1,688 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AntV Infographic Debug Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.infographic-render-container {
|
||||
height: 800px;
|
||||
border: 1px dashed #ccc;
|
||||
border-radius: 4px;
|
||||
margin-top: 20px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background: #e0f2fe;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
/* 保留换行符 */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>AntV Infographic 综合测试台</h1>
|
||||
<div class="controls">
|
||||
<label>选择测试用例:</label>
|
||||
<select id="case-selector">
|
||||
<option value="case-list-basic">Case A: List Basic (Standard)</option>
|
||||
<option value="case-tree-vertical">Case B: Tree Vertical (Hierarchy)</option>
|
||||
<option value="case-theme-extended">Case C: Theme Extended (Bg/Text Color)</option>
|
||||
<option value="case-tree-horizontal">Case D: Tree Horizontal</option>
|
||||
<option value="case-list-children">Case E: List with Children</option>
|
||||
<option value="case-tree-indented">Case F: Indented Tree</option>
|
||||
<option value="case-mindmap">Case G: Mindmap</option>
|
||||
<option value="case-l">Case L: List Grid (MiniMax)</option>
|
||||
<option value="case-list-vertical">Case H: List Vertical</option>
|
||||
<option value="case-statistic-card">Case I: Statistic Card</option>
|
||||
<option value="case-k">Case K: 柠檬霜 Controversy (Roadmap)</option>
|
||||
<option value="case-l">Case L: List Grid (MiniMax)</option>
|
||||
<option value="case-llm-list-grid">LLM: List Grid (Features)</option>
|
||||
<option value="case-llm-tree-vertical">LLM: Tree Vertical (Hierarchy)</option>
|
||||
<option value="case-llm-statistic-card">LLM: Statistic Card (Metrics)</option>
|
||||
<option value="case-llm-mindmap">LLM: Mindmap (Brainstorming)</option>
|
||||
<option value="case-chart-bar">Case M: Chart Bar</option>
|
||||
<option value="case-compare-swot">Case N: Comparison SWOT</option>
|
||||
<option value="case-relation-sankey">Case O: Relationship Sankey</option>
|
||||
<option value="case-quadrant-quarter">Case P: Quadrant Analysis</option>
|
||||
</select>
|
||||
<button onclick="runTest()">运行测试</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="status-log">准备就绪...</div>
|
||||
<div id="infographic-container-test" class="infographic-render-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Case A: 基础列表 -->
|
||||
<script type="text/template" id="case-list-basic">
|
||||
infographic list-row-simple-horizontal-arrow
|
||||
data
|
||||
title 基础列表测试
|
||||
items
|
||||
- label 步骤一
|
||||
icon ref:search:one
|
||||
- label 步骤二
|
||||
icon ref:search:two
|
||||
theme
|
||||
colorPrimary #3b82f6
|
||||
palette #3b82f6 #8b5cf6
|
||||
</script>
|
||||
|
||||
<!-- Case B: 垂直树 (尝试添加 ID) -->
|
||||
<script type="text/template" id="case-tree-vertical">
|
||||
infographic tree-vertical
|
||||
data
|
||||
title 垂直树测试
|
||||
items
|
||||
- label 根节点
|
||||
id root
|
||||
icon ref:search:root
|
||||
children
|
||||
- label 子节点 A
|
||||
id child-a
|
||||
- label 子节点 B
|
||||
id child-b
|
||||
theme
|
||||
colorPrimary #10b981
|
||||
palette #10b981 #34d399
|
||||
</script>
|
||||
|
||||
<!-- Case C: 扩展主题 (测试清理逻辑) -->
|
||||
<script type="text/template" id="case-theme-extended">
|
||||
infographic list-row-simple-horizontal-arrow
|
||||
data
|
||||
title 扩展主题测试
|
||||
items
|
||||
- label 暗色模式
|
||||
icon ref:search:moon
|
||||
theme
|
||||
colorPrimary #6366f1
|
||||
palette #6366f1 #8b5cf6
|
||||
backgroundColor #1e293b
|
||||
textColor #f8fafc
|
||||
roughness 0.5
|
||||
</script>
|
||||
|
||||
<!-- Case D: 水平树 -->
|
||||
<script type="text/template" id="case-tree-horizontal">
|
||||
infographic tree-horizontal
|
||||
data
|
||||
title 水平树测试
|
||||
items
|
||||
- label 根节点
|
||||
id root-h
|
||||
children
|
||||
- label 子节点 1
|
||||
id child-1
|
||||
- label 子节点 2
|
||||
id child-2
|
||||
theme
|
||||
colorPrimary #f59e0b
|
||||
</script>
|
||||
|
||||
<!-- Case E: 列表带子节点 (测试兼容性) -->
|
||||
<script type="text/template" id="case-list-children">
|
||||
infographic list-row-simple-horizontal-arrow
|
||||
data
|
||||
title 列表带子节点
|
||||
items
|
||||
- label 父项 1
|
||||
children
|
||||
- label 子项 1.1
|
||||
- label 父项 2
|
||||
theme
|
||||
colorPrimary #ec4899
|
||||
</script>
|
||||
|
||||
<!-- Case F: Indented Tree (缩进树) -->
|
||||
<script type="text/template" id="case-tree-indented">
|
||||
infographic tree-indented
|
||||
data
|
||||
title 缩进树测试
|
||||
items
|
||||
- label 根节点
|
||||
id root-i
|
||||
children
|
||||
- label 子节点 1
|
||||
id child-i-1
|
||||
- label 子节点 2
|
||||
id child-i-2
|
||||
theme
|
||||
colorPrimary #8b5cf6
|
||||
</script>
|
||||
|
||||
<!-- Case G: Mindmap -->
|
||||
<script type="text/template" id="case-mindmap">
|
||||
infographic mindmap
|
||||
data
|
||||
title 脑图测试
|
||||
items
|
||||
- label 中心主题
|
||||
children
|
||||
- label 主题 A
|
||||
children
|
||||
- label 子主题 A1
|
||||
- label 子主题 A2
|
||||
- label 主题 B
|
||||
theme
|
||||
colorPrimary #06b6d4
|
||||
</script>
|
||||
|
||||
<!-- Case H: List Vertical (垂直列表) -->
|
||||
<script type="text/template" id="case-list-vertical">
|
||||
infographic list-vertical
|
||||
data
|
||||
title 垂直列表测试
|
||||
items
|
||||
- label 第一项
|
||||
desc 描述文本 1
|
||||
icon ref:search:one
|
||||
- label 第二项
|
||||
desc 描述文本 2
|
||||
icon ref:search:two
|
||||
theme
|
||||
colorPrimary #8b5cf6
|
||||
</script>
|
||||
|
||||
<!-- Case I: Statistic Card (统计卡片) -->
|
||||
<script type="text/template" id="case-statistic-card">
|
||||
infographic statistic-card
|
||||
data
|
||||
title 核心指标
|
||||
items
|
||||
- label 总用户数
|
||||
value 1,234
|
||||
icon ref:search:users
|
||||
- label 日活
|
||||
value 89%
|
||||
icon ref:search:activity
|
||||
theme
|
||||
colorPrimary #10b981
|
||||
</script>
|
||||
|
||||
<!-- Case J: Official Tree Example -->
|
||||
<script type="text/template" id="case-official-tree">
|
||||
infographic hierarchy-tree-tech-style-capsule-item
|
||||
data
|
||||
title 官方树形示例
|
||||
items
|
||||
- label 根节点
|
||||
icon ref:search:root
|
||||
children
|
||||
- label 子节点 A
|
||||
icon ref:search:a
|
||||
children
|
||||
- label 叶子 A1
|
||||
- label 叶子 A2
|
||||
- label 子节点 B
|
||||
icon ref:search:b
|
||||
theme
|
||||
colorPrimary #1677ff
|
||||
</script>
|
||||
|
||||
<!-- Case K: User Fail Case (Lemon Frost) -->
|
||||
<script type="text/template" id="case-user-fail">
|
||||
infographic list-vertical
|
||||
data
|
||||
title 柠檬霜豹纹守宫:美丽的诅咒
|
||||
desc 揭露极端审美背后的遗传缺陷、伦理代价与商业真相
|
||||
items
|
||||
- label 审美霸权与病态追求
|
||||
desc 追求极致黄色而筛选出致癌基因,让生命背负终身癌症风险,是伦理上的霸权行为。
|
||||
icon mdi/eye-off-outline
|
||||
- label 致命的基因多效性
|
||||
desc 与“谜”品系不同,柠檬霜导致的是实体肿瘤溃烂。致癌机制与颜色性状不可剥离。
|
||||
icon mdi/dna
|
||||
- label 商业层面的误导欺诈
|
||||
desc 所谓“低肿瘤率”或“改良版”多为营销谎言。只要表现出性状,就注定携带致癌机制。
|
||||
icon mdi/alert-octagon
|
||||
- label 科学研究与宠物价值
|
||||
desc 其价值应仅限于作为黑色素瘤的医学研究模型,而非在宠物交易市场中流通。
|
||||
icon mdi/microscope
|
||||
- label 行动呼吁:坚决拒绝
|
||||
desc 拒绝购买与繁育,将柠檬霜留在教科书中作为警示,而非留在饲养箱中承受痛苦。
|
||||
icon mdi/cancel
|
||||
theme
|
||||
colorPrimary #FFD700
|
||||
colorBg #1A1A1A
|
||||
textColor #FFFFFF
|
||||
palette #FFD700 #FF4D4F #E6E6E6
|
||||
stylize rough
|
||||
roughness 0.2
|
||||
</script>
|
||||
|
||||
<!-- Case L: List Grid (MiniMax) -->
|
||||
<script type="text/template" id="case-l">
|
||||
list-grid
|
||||
data
|
||||
title MiniMax 2025 模型矩阵全解析
|
||||
desc 以 abab 7 为核心的国产顶尖大模型
|
||||
items
|
||||
- label 极致 MoE 架构优化
|
||||
desc 国内首批 MoE 路线坚定者。
|
||||
icon mdi/cpu-64-bit
|
||||
- label Video-01 视频生成
|
||||
desc 杀手锏级多模态能力。
|
||||
icon mdi/movie-filter
|
||||
- label 情感陪伴与角色扮演
|
||||
desc 源自星野基因。
|
||||
icon mdi/emoticon-heart
|
||||
- label 超长上下文与精准召回
|
||||
desc 支持 128k+ 窗口。
|
||||
icon mdi/database-search
|
||||
theme
|
||||
colorPrimary #6366f1
|
||||
colorBg #ffffff
|
||||
textColor #1f2937
|
||||
</script>
|
||||
|
||||
<!-- LLM Generated Cases for Verification -->
|
||||
<script type="text/template" id="case-llm-list-grid">
|
||||
infographic list-grid
|
||||
data
|
||||
title MiniMax 2025 模型矩阵全解析
|
||||
desc 极致架构与多模态能力的全面进阶
|
||||
items
|
||||
- label 极致 MoE 架构优化
|
||||
desc 国内首批 MoE 路线坚定者,兼顾模型性能与推理效率
|
||||
icon mdi/layers-triple
|
||||
- label Video-01 视频生成
|
||||
desc 杀手锏级多模态能力,支持高品质视频内容创作
|
||||
icon mdi/video-vintage
|
||||
- label 情感陪伴与角色扮演
|
||||
desc 源自星野基因,提供深度的情感连接与人格化交互
|
||||
icon mdi/robot-happy
|
||||
- label 超长上下文与精准召回
|
||||
desc 支持 128k+ 超长窗口,实现海量信息的精准处理
|
||||
icon mdi/file-find
|
||||
theme
|
||||
colorPrimary #0052D9
|
||||
colorBg #F2F3F5
|
||||
palette #0052D9 #2BA471 #E37318 #D54941
|
||||
stylize flat
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-llm-tree-vertical">
|
||||
infographic tree-vertical
|
||||
data
|
||||
title 公司组织架构
|
||||
desc 2025年度企业内部编制示意图
|
||||
items
|
||||
- label 研发部
|
||||
icon mdi/xml
|
||||
children
|
||||
- label 后端组
|
||||
icon mdi/server
|
||||
- label 前端组
|
||||
icon mdi/language-javascript
|
||||
- label 市场部
|
||||
icon mdi/chart-bell-curve
|
||||
children
|
||||
- label 销售组
|
||||
icon mdi/account-cash
|
||||
- label 推广组
|
||||
icon mdi/bullhorn
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-llm-statistic-card">
|
||||
infographic statistic-card
|
||||
data
|
||||
title 2024年Q4 核心指标
|
||||
desc 统计日期:2024年第四季度
|
||||
items
|
||||
- label 总用户数
|
||||
value 1,234,567
|
||||
icon mdi/account-group
|
||||
- label 日活跃用户
|
||||
value 89%
|
||||
icon mdi/chart-line
|
||||
- label 营收增长
|
||||
value +45%
|
||||
icon mdi/trending-up
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-llm-mindmap">
|
||||
infographic mindmap
|
||||
data
|
||||
title 人工智能应用领域
|
||||
desc 核心应用场景分类与展示
|
||||
items
|
||||
- label 生成式 AI
|
||||
icon mdi/brain
|
||||
children
|
||||
- label 文本生成
|
||||
icon mdi/text-recognition
|
||||
- label 图像生成
|
||||
icon mdi/image-multiple
|
||||
- label 视频生成
|
||||
icon mdi/video-input-component
|
||||
- label 预测性 AI
|
||||
icon mdi/chart-line
|
||||
children
|
||||
- label 股市预测
|
||||
icon mdi/finance
|
||||
- label 天气预报
|
||||
icon mdi/weather-partly-cloudy
|
||||
- label 决策 AI
|
||||
icon mdi/robot
|
||||
children
|
||||
- label 自动驾驶
|
||||
icon mdi/car-autonomous
|
||||
- label 游戏博弈
|
||||
icon mdi/controller-classic
|
||||
theme
|
||||
colorPrimary #2F54EB
|
||||
colorBg #FFFFFF
|
||||
</script>
|
||||
|
||||
<!-- New Styles Test Cases -->
|
||||
<script type="text/template" id="case-chart-bar">
|
||||
infographic chart-bar
|
||||
data
|
||||
title 2023年季度营收分析
|
||||
desc 单位:亿元
|
||||
items
|
||||
- label 第一季度
|
||||
value 12.5
|
||||
- label 第二季度
|
||||
value 15.8
|
||||
- label 第三季度
|
||||
value 14.2
|
||||
- label 第四季度
|
||||
value 18.9
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-relation-circle">
|
||||
infographic relation-circle
|
||||
data
|
||||
title 生态循环
|
||||
items
|
||||
- label 生产者
|
||||
- label 消费者
|
||||
- label 分解者
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-compare-swot">
|
||||
infographic compare-swot
|
||||
data
|
||||
title 某初创咖啡品牌 SWOT 分析
|
||||
items
|
||||
- label 优势
|
||||
children
|
||||
- label 产品口味独特
|
||||
- label 选址精准
|
||||
- label 劣势
|
||||
children
|
||||
- label 品牌知名度低
|
||||
- label 资金压力大
|
||||
- label 机会
|
||||
children
|
||||
- label 线上外卖市场增长
|
||||
- label 年轻人对精品咖啡需求增加
|
||||
- label 威胁
|
||||
children
|
||||
- label 行业巨头价格战
|
||||
- label 原材料成本上涨
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-relation-sankey">
|
||||
infographic relation-sankey
|
||||
data
|
||||
title 家庭月度开支流向
|
||||
desc 2025年12月家庭总收入 10000 元分配明细
|
||||
items
|
||||
- label 总收入
|
||||
value 10000
|
||||
children
|
||||
- label 房贷
|
||||
value 4000
|
||||
- label 日常消费
|
||||
value 3000
|
||||
children
|
||||
- label 餐饮
|
||||
value 2000
|
||||
- label 交通
|
||||
value 1000
|
||||
- label 教育培训
|
||||
value 2000
|
||||
- label 存入银行
|
||||
value 1000
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-quadrant-quarter">
|
||||
infographic quadrant-quarter
|
||||
data
|
||||
title 个人任务象限分析
|
||||
desc 2025年12月28日 任务优先级管理
|
||||
items
|
||||
- label 修复线上紧急 Bug
|
||||
desc 紧急且重要:立即优先处理
|
||||
illus mdi/bug
|
||||
- label 准备下午的客户会议
|
||||
desc 紧急且重要:核心商务产出
|
||||
illus mdi/account-group
|
||||
- label 制定年度学习计划
|
||||
desc 重要不紧急:长期价值积累
|
||||
illus mdi/calendar-check
|
||||
- label 健身锻炼
|
||||
desc 重要不紧急:身体健康投资
|
||||
illus mdi/dumbbell
|
||||
- label 回复琐碎的邮件
|
||||
desc 紧急不重要:低价值干扰
|
||||
illus mdi/email-outline
|
||||
- label 接听推销电话
|
||||
desc 紧急不重要:时间黑洞
|
||||
illus mdi/phone-cancel
|
||||
- label 刷社交媒体
|
||||
desc 不紧急不重要:消遣娱乐
|
||||
illus mdi/instagram
|
||||
- label 看无聊的综艺
|
||||
desc 不紧急不重要:无谓消耗
|
||||
illus mdi/television-classic
|
||||
</script>
|
||||
|
||||
<script type="text/template" id="case-compare-binary">
|
||||
infographic compare-binary
|
||||
data
|
||||
title IDE vs Open WebUI
|
||||
desc 核心差异对比
|
||||
items
|
||||
- label IDE
|
||||
desc 代码工程中心
|
||||
children
|
||||
- label 深度上下文
|
||||
- label 实时补全
|
||||
- label Open WebUI
|
||||
desc 对话交互中心
|
||||
children
|
||||
- label 模型管理
|
||||
- label 知识库 RAG
|
||||
</script>
|
||||
|
||||
<script src="https://unpkg.com/@antv/infographic@latest/dist/infographic.min.js"></script>
|
||||
<script>
|
||||
function log(msg) {
|
||||
console.log(msg);
|
||||
const el = document.getElementById('status-log');
|
||||
el.textContent += '\n' + msg;
|
||||
}
|
||||
|
||||
function logError(err) {
|
||||
console.error('Detailed Error:', err);
|
||||
let msg = `❌ 内部错误: ${err.message}`;
|
||||
if (err.stack) msg += `\nStack: ${err.stack}`;
|
||||
for (const key in err) {
|
||||
if (key !== 'message' && key !== 'stack') {
|
||||
msg += `\n${key}: ${JSON.stringify(err[key])}`;
|
||||
}
|
||||
}
|
||||
log(msg);
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('status-log').textContent = '准备就绪...';
|
||||
}
|
||||
|
||||
window.listTemplates = () => {
|
||||
clearLog();
|
||||
log('[Explore] 正在获取模版列表...');
|
||||
if (typeof AntVInfographic !== 'undefined') {
|
||||
if (typeof AntVInfographic.getTemplates === 'function') {
|
||||
const templates = AntVInfographic.getTemplates();
|
||||
log(`[Explore] getTemplates() result (${templates.length}):\n${JSON.stringify(templates, null, 2)}`);
|
||||
console.log(templates);
|
||||
} else {
|
||||
log('❌ AntVInfographic.getTemplates is not a function.');
|
||||
log('Available keys: ' + Object.keys(AntVInfographic).join(', '));
|
||||
}
|
||||
} else {
|
||||
log('❌ AntVInfographic is undefined.');
|
||||
}
|
||||
};
|
||||
|
||||
window.runTest = () => {
|
||||
clearLog();
|
||||
const selector = document.getElementById('case-selector');
|
||||
const caseId = selector.value;
|
||||
const sourceEl = document.getElementById(caseId);
|
||||
const containerEl = document.getElementById('infographic-container-test');
|
||||
|
||||
if (!sourceEl) return log('❌ 未找到测试用例');
|
||||
|
||||
// 重置容器
|
||||
containerEl.innerHTML = '';
|
||||
containerEl.style = '';
|
||||
|
||||
let syntaxContent = sourceEl.textContent.trim();
|
||||
log(`[Test] 运行用例: ${caseId} (${selector.options[selector.selectedIndex].text})`);
|
||||
log(`[Step 1] 原始内容:\n${syntaxContent}`);
|
||||
|
||||
// --- 模拟插件的清理逻辑 ---
|
||||
const bgMatch = syntaxContent.match(/backgroundColor\s+(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|[a-zA-Z]+)/);
|
||||
if (bgMatch && bgMatch[1]) {
|
||||
containerEl.style.backgroundColor = bgMatch[1];
|
||||
}
|
||||
const textMatch = syntaxContent.match(/textColor\s+(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3}|[a-zA-Z]+)/);
|
||||
if (textMatch && textMatch[1]) {
|
||||
containerEl.style.color = textMatch[1];
|
||||
}
|
||||
|
||||
// 使用更强的正则:匹配 stylize 及其后续缩进的行
|
||||
const nl = String.fromCharCode(10);
|
||||
const cleanRegex = new RegExp('^\\s*(roughness|stylize|backgroundColor|textColor|colorBg).*(' + nl + '\\s+.*)*', 'gm');
|
||||
if (cleanRegex.test(syntaxContent)) {
|
||||
log('[Fix] 移除扩展 Theme 属性...');
|
||||
syntaxContent = syntaxContent.replace(cleanRegex, '');
|
||||
}
|
||||
|
||||
// 2. 模板映射配置
|
||||
const TEMPLATE_MAPPING = {
|
||||
// 列表与层级
|
||||
'list-grid': 'list-grid-compact-card',
|
||||
'list-vertical': 'list-column-simple-vertical-arrow',
|
||||
'tree-vertical': 'hierarchy-tree-tech-style-capsule-item',
|
||||
'tree-horizontal': 'hierarchy-tree-lr-tech-style-capsule-item',
|
||||
'mindmap': 'hierarchy-mindmap-branch-gradient-capsule-item',
|
||||
|
||||
// 顺序与关系
|
||||
'sequence-roadmap': 'sequence-roadmap-vertical-simple',
|
||||
'sequence-zigzag': 'sequence-horizontal-zigzag-simple',
|
||||
'sequence-horizontal': 'sequence-horizontal-zigzag-simple',
|
||||
'relation-sankey': 'relation-sankey-simple', // 暂无直接对应,保留原值或需移除
|
||||
'relation-circle': 'relation-circle-icon-badge',
|
||||
|
||||
// 对比与分析
|
||||
'compare-binary': 'compare-binary-horizontal-simple-vs',
|
||||
'compare-swot': 'compare-swot',
|
||||
'compare-table': 'compare-table-simple', // 暂无直接对应
|
||||
'quadrant-quarter': 'quadrant-quarter-simple-card',
|
||||
|
||||
// 图表与数据
|
||||
'statistic-card': 'list-grid-compact-card',
|
||||
'chart-bar': 'chart-bar-plain-text',
|
||||
'chart-column': 'chart-column-simple',
|
||||
'chart-line': 'chart-line-plain-text',
|
||||
'chart-area': 'chart-area-simple', // 暂无直接对应
|
||||
'chart-pie': 'chart-pie-plain-text',
|
||||
'chart-doughnut': 'chart-pie-donut-plain-text'
|
||||
};
|
||||
|
||||
// 3. 应用映射策略
|
||||
for (const [key, value] of Object.entries(TEMPLATE_MAPPING)) {
|
||||
const regex = new RegExp(`infographic\\s+${key}(?=\\s|$)`, 'i');
|
||||
if (regex.test(syntaxContent)) {
|
||||
log(`[Fix] 自动映射模板: ${key} -> ${value}`);
|
||||
syntaxContent = syntaxContent.replace(regex, `infographic ${value}`);
|
||||
break; // 找到一个匹配后即停止
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底检查:确保以 infographic 开头
|
||||
if (!syntaxContent.trim().toLowerCase().startsWith('infographic')) {
|
||||
const firstWord = syntaxContent.trim().split(/\s+/)[0].toLowerCase();
|
||||
if (!['data', 'theme', 'design', 'items'].includes(firstWord)) {
|
||||
log('[Fix] 检测到缺失 infographic 前缀,自动补全');
|
||||
syntaxContent = 'infographic ' + syntaxContent;
|
||||
}
|
||||
}
|
||||
|
||||
syntaxContent = syntaxContent.trim();
|
||||
log(`[Step 2] 清理后内容:\n${syntaxContent}`);
|
||||
|
||||
try {
|
||||
const { Infographic } = AntVInfographic;
|
||||
const instance = new Infographic({
|
||||
container: '#infographic-container-test',
|
||||
padding: 24,
|
||||
});
|
||||
|
||||
instance.on('error', (err) => {
|
||||
logError(err);
|
||||
});
|
||||
|
||||
instance.render(syntaxContent);
|
||||
|
||||
if (instance.node) {
|
||||
log('✅ 渲染成功 (instance.node 存在)');
|
||||
if (!containerEl.querySelector('svg')) {
|
||||
log('⚠️ 自动挂载失败,手动挂载...');
|
||||
containerEl.appendChild(instance.node);
|
||||
}
|
||||
} else {
|
||||
log('❌ 渲染失败 (instance.node 为空)');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
395
plugins/actions/infographic/verify_generation.py
Normal file
395
plugins/actions/infographic/verify_generation.py
Normal file
@@ -0,0 +1,395 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from dotenv import load_dotenv
|
||||
import pathlib
|
||||
|
||||
# Load .env from the same directory as this script
|
||||
env_path = pathlib.Path(__file__).parent / ".env"
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
# =================================================================
|
||||
# Configuration
|
||||
# =================================================================
|
||||
|
||||
API_KEY = os.getenv("OPENAI_API_KEY")
|
||||
BASE_URL = os.getenv("OPENAI_BASE_URL")
|
||||
MODEL = os.getenv("OPENAI_MODEL", "gpt-4o") # Default to gpt-4o if not set
|
||||
|
||||
if not API_KEY or not BASE_URL:
|
||||
print(
|
||||
"Error: OPENAI_API_KEY and OPENAI_BASE_URL environment variables must be set."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# =================================================================
|
||||
# Prompts (Extracted from 信息图.py)
|
||||
# =================================================================
|
||||
|
||||
SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT = """
|
||||
You are a professional infographic design expert who can analyze user-provided text content and convert it into AntV Infographic syntax format.
|
||||
|
||||
## Infographic Syntax Specification
|
||||
|
||||
Infographic syntax is a Mermaid-like declarative syntax for describing infographic templates, data, and themes.
|
||||
|
||||
### Syntax Rules
|
||||
- Entry uses `infographic <template-name>`
|
||||
- Key-value pairs are separated by spaces, **absolutely NO colons allowed**
|
||||
- Use two spaces for indentation
|
||||
- Object arrays use `-` with line breaks
|
||||
|
||||
⚠️ **IMPORTANT WARNING: This is NOT YAML format!**
|
||||
- ❌ Wrong: `children:` `items:` `data:` (with colons)
|
||||
- ✅ Correct: `children` `items` `data` (without colons)
|
||||
|
||||
### Template Library & Selection Guide
|
||||
|
||||
#### 1. List & Hierarchy (Text-heavy)
|
||||
- **Linear & Short (Steps/Phases)** -> `list-row-horizontal-icon-arrow`
|
||||
- **Linear & Long (Rankings/Details)** -> `list-vertical`
|
||||
- **Grouped / Parallel (Features/Catalog)** -> `list-grid`
|
||||
- **Hierarchical (Org Chart/Taxonomy)** -> `tree-vertical` or `tree-horizontal`
|
||||
- **Central Idea (Brainstorming)** -> `mindmap`
|
||||
|
||||
#### 2. Sequence & Relationship (Flow-based)
|
||||
- **Time-based (History/Plan)** -> `sequence-roadmap-vertical-simple`
|
||||
- **Process Flow (Complex)** -> `sequence-zigzag` or `sequence-horizontal`
|
||||
- **Resource Flow / Distribution** -> `relation-sankey`
|
||||
- **Circular Relationship** -> `relation-circle`
|
||||
|
||||
#### 3. Comparison & Analysis
|
||||
- **Binary Comparison (A vs B)** -> `compare-binary`
|
||||
- **SWOT Analysis** -> `compare-swot`
|
||||
- **Multi-item Comparison Table** -> `compare-table`
|
||||
- **Quadrant Analysis (Importance vs Urgency)** -> `quadrant-quarter`
|
||||
|
||||
#### 4. Charts & Data (Metric-heavy)
|
||||
- **Key Metrics / Data Cards** -> `statistic-card`
|
||||
- **Distribution / Comparison** -> `chart-bar` or `chart-column`
|
||||
- **Trend over Time** -> `chart-line` or `chart-area`
|
||||
- **Proportion / Part-to-Whole** -> `chart-pie` or `chart-doughnut`
|
||||
|
||||
### Infographic Syntax Guide
|
||||
|
||||
#### 1. Structure
|
||||
- **Entry**: `infographic <template-name>`
|
||||
- **Blocks**: `data`, `theme`, `design` (optional)
|
||||
- **Format**: Key-value pairs separated by spaces, 2-space indentation.
|
||||
- **Arrays**: Object arrays use `-` (newline), simple arrays use inline values.
|
||||
|
||||
#### 2. Data Block (`data`)
|
||||
- `title`: Main title
|
||||
- `desc`: Subtitle or description
|
||||
- `items`: List of data items
|
||||
- - `label`: Item title
|
||||
- - `value`: Numerical value (required for Charts/Stats)
|
||||
- - `desc`: Item description (optional)
|
||||
- - `icon`: Icon name (e.g., `mdi/rocket-launch`)
|
||||
- - `time`: Time label (Optional, for Roadmap/Sequence)
|
||||
- - `children`: Nested items (ONLY for Tree/Mindmap/Sankey/SWOT)
|
||||
- - `illus`: Illustration name (ONLY for Quadrant)
|
||||
|
||||
#### 3. Theme Block (`theme`)
|
||||
- `colorPrimary`: Main color (Hex)
|
||||
- `colorBg`: Background color (Hex)
|
||||
- `palette`: Color list (Space separated)
|
||||
- `textColor`: Text color (Hex)
|
||||
- `stylize`: Style effect (e.g., `rough`, `flat`)
|
||||
|
||||
### Examples
|
||||
|
||||
#### Chart (Bar Chart)
|
||||
infographic chart-bar
|
||||
data
|
||||
title Revenue Growth
|
||||
desc Monthly revenue in 2024
|
||||
items
|
||||
- label Jan
|
||||
value 1200
|
||||
- label Feb
|
||||
value 1500
|
||||
- label Mar
|
||||
value 1800
|
||||
|
||||
#### Comparison (SWOT)
|
||||
infographic compare-swot
|
||||
data
|
||||
title Project SWOT
|
||||
items
|
||||
- label Strengths
|
||||
children
|
||||
- label Strong team
|
||||
- label Innovative tech
|
||||
- label Weaknesses
|
||||
children
|
||||
- label Limited budget
|
||||
- label Opportunities
|
||||
children
|
||||
- label Emerging market
|
||||
- label Threats
|
||||
children
|
||||
- label High competition
|
||||
|
||||
#### Relationship (Sankey)
|
||||
infographic relation-sankey
|
||||
data
|
||||
title Energy Flow
|
||||
items
|
||||
- label Solar
|
||||
value 100
|
||||
children
|
||||
- label Grid
|
||||
value 60
|
||||
- label Battery
|
||||
value 40
|
||||
- label Wind
|
||||
value 80
|
||||
children
|
||||
- label Grid
|
||||
value 80
|
||||
|
||||
#### Quadrant (Importance vs Urgency)
|
||||
infographic quadrant-quarter
|
||||
data
|
||||
title Task Management
|
||||
items
|
||||
- label Critical Bug
|
||||
desc Fix immediately
|
||||
illus mdi/bug
|
||||
- label Feature Request
|
||||
desc Plan for next sprint
|
||||
illus mdi/star
|
||||
|
||||
### Output Rules
|
||||
1. **Strict Syntax**: Follow the indentation and formatting rules exactly.
|
||||
2. **No Explanations**: Output ONLY the syntax code block.
|
||||
3. **Language**: Use the user's requested language for content.
|
||||
"""
|
||||
|
||||
USER_PROMPT_GENERATE_INFOGRAPHIC = """
|
||||
请分析以下文本内容,将其核心信息转换为 AntV Infographic 语法格式。
|
||||
|
||||
---
|
||||
**用户上下文信息:**
|
||||
用户姓名: {user_name}
|
||||
当前日期时间: {current_date_time_str}
|
||||
用户语言: {user_language}
|
||||
---
|
||||
|
||||
**文本内容:**
|
||||
{long_text_content}
|
||||
|
||||
请根据文本特点选择最合适的信息图模板,并输出规范的 infographic 语法。注意保持正确的缩进格式(两个空格)。
|
||||
"""
|
||||
|
||||
# =================================================================
|
||||
# Test Cases
|
||||
# =================================================================
|
||||
|
||||
TEST_CASES = [
|
||||
{
|
||||
"name": "List Grid (Features)",
|
||||
"content": """
|
||||
MiniMax 2025 模型矩阵全解析:
|
||||
1. 极致 MoE 架构优化:国内首批 MoE 路线坚定者。
|
||||
2. Video-01 视频生成:杀手锏级多模态能力。
|
||||
3. 情感陪伴与角色扮演:源自星野基因。
|
||||
4. 超长上下文与精准召回:支持 128k+ 窗口。
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Tree Vertical (Hierarchy)",
|
||||
"content": """
|
||||
公司组织架构:
|
||||
- 研发部
|
||||
- 后端组
|
||||
- 前端组
|
||||
- 市场部
|
||||
- 销售组
|
||||
- 推广组
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Statistic Card (Metrics)",
|
||||
"content": """
|
||||
2024年Q4 核心指标:
|
||||
- 总用户数:1,234,567
|
||||
- 日活跃用户:89%
|
||||
- 营收增长:+45%
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Mindmap (Brainstorming)",
|
||||
"content": """
|
||||
人工智能的应用领域:
|
||||
- 生成式 AI:文本生成、图像生成、视频生成
|
||||
- 预测性 AI:股市预测、天气预报
|
||||
- 决策 AI:自动驾驶、游戏博弈
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "SWOT Analysis",
|
||||
"content": """
|
||||
某初创咖啡品牌的 SWOT 分析:
|
||||
优势:产品口味独特,选址精准。
|
||||
劣势:品牌知名度低,资金压力大。
|
||||
机会:线上外卖市场增长,年轻人对精品咖啡需求增加。
|
||||
威胁:行业巨头价格战,原材料成本上涨。
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Sankey (Relationship)",
|
||||
"content": """
|
||||
家庭月度开支流向:
|
||||
总收入 10000 元。
|
||||
其中 4000 元用于房贷。
|
||||
3000 元用于日常消费(包括 2000 元餐饮,1000 元交通)。
|
||||
2000 元用于教育培训。
|
||||
1000 元存入银行。
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Quadrant (Analysis)",
|
||||
"content": """
|
||||
个人任务象限分析:
|
||||
- 紧急且重要:修复线上紧急 Bug,准备下午的客户会议。
|
||||
- 重要不紧急:制定年度学习计划,健身锻炼。
|
||||
- 紧急不重要:回复琐碎的邮件,接听推销电话。
|
||||
- 不紧急不重要:刷社交媒体,看无聊的综艺。
|
||||
""",
|
||||
},
|
||||
{
|
||||
"name": "Chart Bar (Data)",
|
||||
"content": """
|
||||
2023年各季度营收情况(亿元):
|
||||
第一季度:12.5
|
||||
第二季度:15.8
|
||||
第三季度:14.2
|
||||
第四季度:18.9
|
||||
""",
|
||||
},
|
||||
]
|
||||
|
||||
# =================================================================
|
||||
# Helper Functions
|
||||
# =================================================================
|
||||
|
||||
|
||||
def generate_infographic(content):
|
||||
now = datetime.now()
|
||||
current_date_time_str = now.strftime("%Y年%m月%d日 %H:%M:%S")
|
||||
|
||||
formatted_user_prompt = USER_PROMPT_GENERATE_INFOGRAPHIC.format(
|
||||
user_name="TestUser",
|
||||
current_date_time_str=current_date_time_str,
|
||||
user_language="zh-CN",
|
||||
long_text_content=content,
|
||||
)
|
||||
|
||||
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
|
||||
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT_INFOGRAPHIC_ASSISTANT},
|
||||
{"role": "user", "content": formatted_user_prompt},
|
||||
],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/chat/completions", headers=headers, json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()["choices"][0]["message"]["content"]
|
||||
except Exception as e:
|
||||
print(f"API Request Failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def validate_syntax(syntax):
|
||||
if not syntax:
|
||||
return False, "Empty output"
|
||||
|
||||
# Basic checks
|
||||
if "infographic" not in syntax.lower():
|
||||
return False, "Missing 'infographic' keyword"
|
||||
|
||||
if "data" not in syntax.lower() and "items" not in syntax.lower():
|
||||
return False, "Missing 'data' or 'items' block"
|
||||
|
||||
# Check for colons in keys (simple heuristic)
|
||||
lines = syntax.split("\n")
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
# Ignore lines that are likely values or descriptions containing colons
|
||||
first_word = stripped.split()[0]
|
||||
if first_word in ["title", "desc", "label", "value", "time", "icon"]:
|
||||
continue # These can have colons in value
|
||||
|
||||
# Check for key: pattern at start of line
|
||||
if re.match(r"^\w+:", stripped):
|
||||
return False, f"Found colon in key: {stripped}"
|
||||
|
||||
return True, "Syntax looks valid"
|
||||
|
||||
|
||||
# =================================================================
|
||||
# Main Execution
|
||||
# =================================================================
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def main():
|
||||
print(f"Starting Infographic Generation Verification...")
|
||||
print(f"API: {BASE_URL}")
|
||||
print(f"Model: {MODEL}")
|
||||
print("-" * 50)
|
||||
|
||||
results = []
|
||||
|
||||
for case in TEST_CASES:
|
||||
print(f"\nTesting Case: {case['name']}")
|
||||
print("Generating...")
|
||||
|
||||
output = generate_infographic(case["content"])
|
||||
|
||||
if output:
|
||||
# Clean output (remove markdown code blocks if present)
|
||||
clean_output = output
|
||||
if "```" in output:
|
||||
match = re.search(
|
||||
r"```(?:infographic|mermaid)?\s*(.*?)\s*```", output, re.DOTALL
|
||||
)
|
||||
if match:
|
||||
clean_output = match.group(1).strip()
|
||||
|
||||
print(f"Output Preview:\n{clean_output[:200]}...")
|
||||
|
||||
is_valid, message = validate_syntax(clean_output)
|
||||
if is_valid:
|
||||
print(f"✅ Validation Passed: {message}")
|
||||
results.append({"name": case["name"], "status": "PASS"})
|
||||
else:
|
||||
print(f"❌ Validation Failed: {message}")
|
||||
print(f"Full Output:\n{output}")
|
||||
results.append({"name": case["name"], "status": "FAIL"})
|
||||
else:
|
||||
print("❌ Generation Failed")
|
||||
results.append({"name": case["name"], "status": "ERROR"})
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("Summary")
|
||||
print("=" * 50)
|
||||
for res in results:
|
||||
print(f"{res['name']}: {res['status']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1155
plugins/actions/infographic/信息图.py
Normal file
1155
plugins/actions/infographic/信息图.py
Normal file
File diff suppressed because one or more lines are too long
@@ -95,6 +95,10 @@ class Action:
|
||||
default=False,
|
||||
description="Whether to force clear previous plugin results (if True, overwrites instead of merging).",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -111,13 +115,32 @@ class Action:
|
||||
if not __event_emitter__:
|
||||
return body
|
||||
|
||||
# Get the last user message
|
||||
# Get messages based on MESSAGE_COUNT
|
||||
messages = body.get("messages", [])
|
||||
if not messages:
|
||||
return body
|
||||
|
||||
# Usually the action is triggered on the last message
|
||||
target_message = messages[-1]["content"]
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"User"
|
||||
if role == "user"
|
||||
else "Assistant" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} Message {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
return body
|
||||
|
||||
target_message = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
# Check text length
|
||||
text_length = len(target_message)
|
||||
@@ -140,9 +163,18 @@ class Action:
|
||||
await self._emit_notification(
|
||||
__event_emitter__, "⚡ Generating Flash Card...", "info"
|
||||
)
|
||||
await self._emit_status(
|
||||
__event_emitter__, "⚡ Flash Card: Starting generation...", done=False
|
||||
)
|
||||
|
||||
try:
|
||||
# 1. Extract information using LLM
|
||||
await self._emit_status(
|
||||
__event_emitter__,
|
||||
"⚡ Flash Card: Calling AI model to analyze content...",
|
||||
done=False,
|
||||
)
|
||||
|
||||
user_id = __user__.get("id") if __user__ else "default"
|
||||
user_obj = Users.get_user_by_id(user_id)
|
||||
|
||||
@@ -187,6 +219,12 @@ Important Principles:
|
||||
response = await generate_chat_completion(__request__, payload, user_obj)
|
||||
content = response["choices"][0]["message"]["content"]
|
||||
|
||||
await self._emit_status(
|
||||
__event_emitter__,
|
||||
"⚡ Flash Card: AI analysis complete, parsing data...",
|
||||
done=False,
|
||||
)
|
||||
|
||||
# Parse JSON
|
||||
try:
|
||||
# simple cleanup in case of markdown code blocks
|
||||
@@ -198,14 +236,20 @@ Important Principles:
|
||||
card_data = json.loads(content)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse JSON: {e}, content: {content}")
|
||||
await self._emit_status(
|
||||
__event_emitter__, "❌ Flash Card: Data parsing failed", done=True
|
||||
)
|
||||
await self._emit_notification(
|
||||
__event_emitter__,
|
||||
"Failed to generate card data, please try again.",
|
||||
"❌ Failed to generate card data, please try again.",
|
||||
"error",
|
||||
)
|
||||
return body
|
||||
|
||||
# 2. Generate HTML components
|
||||
await self._emit_status(
|
||||
__event_emitter__, "⚡ Flash Card: Rendering card...", done=False
|
||||
)
|
||||
card_content, card_style = self.generate_html_card_components(card_data)
|
||||
|
||||
# 3. Append to message
|
||||
@@ -245,6 +289,9 @@ Important Principles:
|
||||
html_embed_tag = f"```html\n{final_html}\n```"
|
||||
body["messages"][-1]["content"] += f"\n\n{html_embed_tag}"
|
||||
|
||||
await self._emit_status(
|
||||
__event_emitter__, "✅ Flash Card: Generation complete!", done=True
|
||||
)
|
||||
await self._emit_notification(
|
||||
__event_emitter__, "⚡ Flash Card generated successfully!", "success"
|
||||
)
|
||||
@@ -253,8 +300,13 @@ Important Principles:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating knowledge card: {e}")
|
||||
await self._emit_status(
|
||||
__event_emitter__, "❌ Flash Card: Generation failed", done=True
|
||||
)
|
||||
await self._emit_notification(
|
||||
__event_emitter__, f"Error generating knowledge card: {str(e)}", "error"
|
||||
__event_emitter__,
|
||||
f"❌ Error generating knowledge card: {str(e)}",
|
||||
"error",
|
||||
)
|
||||
return body
|
||||
|
||||
@@ -277,6 +329,21 @@ Important Principles:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""Extract text from message content, supporting multimodal message formats"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# Multimodal message: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
|
||||
@@ -92,6 +92,10 @@ class Action:
|
||||
default=False,
|
||||
description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="用于生成的最近消息数量。设置为1仅使用最后一条消息,更大值可包含更多上下文。",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -108,13 +112,32 @@ class Action:
|
||||
if not __event_emitter__:
|
||||
return body
|
||||
|
||||
# Get the last user message
|
||||
# Get messages based on MESSAGE_COUNT
|
||||
messages = body.get("messages", [])
|
||||
if not messages:
|
||||
return body
|
||||
|
||||
# Usually the action is triggered on the last message
|
||||
target_message = messages[-1]["content"]
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"用户"
|
||||
if role == "user"
|
||||
else "助手" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} 消息 {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
return body
|
||||
|
||||
target_message = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
# Check text length
|
||||
text_length = len(target_message)
|
||||
@@ -135,9 +158,14 @@ class Action:
|
||||
|
||||
# Notify user that we are generating the card
|
||||
await self._emit_notification(__event_emitter__, "⚡ 正在生成闪记卡...", "info")
|
||||
await self._emit_status(__event_emitter__, "⚡ 闪记卡: 开始生成...", done=False)
|
||||
|
||||
try:
|
||||
# 1. Extract information using LLM
|
||||
await self._emit_status(
|
||||
__event_emitter__, "⚡ 闪记卡: 正在调用 AI 模型分析内容...", done=False
|
||||
)
|
||||
|
||||
user_id = __user__.get("id") if __user__ else "default"
|
||||
user_obj = Users.get_user_by_id(user_id)
|
||||
|
||||
@@ -182,6 +210,10 @@ class Action:
|
||||
response = await generate_chat_completion(__request__, payload, user_obj)
|
||||
content = response["choices"][0]["message"]["content"]
|
||||
|
||||
await self._emit_status(
|
||||
__event_emitter__, "⚡ 闪记卡: AI 分析完成,正在解析数据...", done=False
|
||||
)
|
||||
|
||||
# Parse JSON
|
||||
try:
|
||||
# simple cleanup in case of markdown code blocks
|
||||
@@ -193,12 +225,18 @@ class Action:
|
||||
card_data = json.loads(content)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse JSON: {e}, content: {content}")
|
||||
await self._emit_status(
|
||||
__event_emitter__, "❌ 闪记卡: 数据解析失败", done=True
|
||||
)
|
||||
await self._emit_notification(
|
||||
__event_emitter__, "生成卡片数据失败,请重试。", "error"
|
||||
__event_emitter__, "❌ 生成卡片数据失败,请重试。", "error"
|
||||
)
|
||||
return body
|
||||
|
||||
# 2. Generate HTML components
|
||||
await self._emit_status(
|
||||
__event_emitter__, "⚡ 闪记卡: 正在渲染卡片...", done=False
|
||||
)
|
||||
card_content, card_style = self.generate_html_card_components(card_data)
|
||||
|
||||
# 3. Append to message
|
||||
@@ -238,6 +276,9 @@ class Action:
|
||||
html_embed_tag = f"```html\n{final_html}\n```"
|
||||
body["messages"][-1]["content"] += f"\n\n{html_embed_tag}"
|
||||
|
||||
await self._emit_status(
|
||||
__event_emitter__, "✅ 闪记卡: 生成完成!", done=True
|
||||
)
|
||||
await self._emit_notification(
|
||||
__event_emitter__, "⚡ 闪记卡生成成功!", "success"
|
||||
)
|
||||
@@ -246,8 +287,9 @@ class Action:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating knowledge card: {e}")
|
||||
await self._emit_status(__event_emitter__, "❌ 闪记卡: 生成失败", done=True)
|
||||
await self._emit_notification(
|
||||
__event_emitter__, f"生成知识卡片时出错: {str(e)}", "error"
|
||||
__event_emitter__, f"❌ 生成知识卡片时出错: {str(e)}", "error"
|
||||
)
|
||||
return body
|
||||
|
||||
@@ -270,6 +312,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""从消息内容中提取文本,支持多模态消息格式"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# 多模态消息: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
title: Smart Mind Map
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iY3VycmVudENvbG9yIi8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iOSIgeDI9IjEyIiB5Mj0iNCIvPgogIDxjaXJjbGUgY3g9IjEyIiBjeT0iMyIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjIwIi8+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIyMSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjkiIHkxPSIxMiIgeDI9IjQiIHkyPSIxMiIvPgogIDxjaXJjbGUgY3g9IjMiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjE1IiB5MT0iMTIiIHgyPSIyMCIgeTI9IjEyIi8+CiAgPGNpcmNsZSBjeD0iMjEiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEwLjUiIHkxPSIxMC41IiB4Mj0iNiIgeTI9IjYiLz4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iNSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEzLjUiIHkxPSIxMC41IiB4Mj0iMTgiIHkyPSI2Ii8+CiAgPGNpcmNsZSBjeD0iMTkiIGN5PSI1IiByPSIxLjUiLz4KICA8bGluZSB4MT0iMTAuNSIgeTE9IjEzLjUiIHgyPSI2IiB5Mj0iMTgiLz4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iMTkiIHI9IjEuNSIvPgogIDxsaW5lIHgxPSIxMy41IiB5MT0iMTMuNSIgeDI9IjE4IiB5Mj0iMTgiLz4KICA8Y2lyY2xlIGN4PSIxOSIgY3k9IjE5IiByPSIxLjUiLz4KPC9zdmc+
|
||||
version: 0.7.3
|
||||
version: 0.7.4
|
||||
description: Intelligently analyzes long texts and generates interactive mind maps, supporting SVG/Markdown export.
|
||||
"""
|
||||
|
||||
@@ -81,17 +81,14 @@ HTML_WRAPPER_TEMPLATE = """
|
||||
width: 100%;
|
||||
}
|
||||
.plugin-item {
|
||||
flex: 1 1 400px; /* Default width, allows shrinking/growing */
|
||||
flex: 1 1 400px; /* Default width, allows stretching */
|
||||
min-width: 300px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.plugin-item:hover {
|
||||
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.plugin-item { flex: 1 1 100%; }
|
||||
@@ -128,7 +125,6 @@ CSS_TEMPLATE_MINDMAP = """
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--card-bg-color);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
height: 100%;
|
||||
@@ -169,7 +165,6 @@ CSS_TEMPLATE_MINDMAP = """
|
||||
background-size: 20px 20px;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -287,7 +282,8 @@ SCRIPT_TEMPLATE_MINDMAP = """
|
||||
try {
|
||||
const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svgEl.style.width = '100%';
|
||||
svgEl.style.height = '500px';
|
||||
svgEl.style.height = 'auto';
|
||||
svgEl.style.minHeight = '300px';
|
||||
containerEl.innerHTML = '';
|
||||
containerEl.appendChild(svgEl);
|
||||
|
||||
@@ -410,6 +406,10 @@ class Action:
|
||||
default=False,
|
||||
description="Whether to force clear previous plugin results (if True, overwrites instead of merging).",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -453,6 +453,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""Extract text from message content, supporting multimodal message formats"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# Multimodal message: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
@@ -544,18 +559,40 @@ class Action:
|
||||
)
|
||||
|
||||
messages = body.get("messages")
|
||||
if (
|
||||
not messages
|
||||
or not isinstance(messages, list)
|
||||
or not messages[-1].get("content")
|
||||
):
|
||||
if not messages or not isinstance(messages, list):
|
||||
error_message = "Unable to retrieve valid user message content."
|
||||
await self._emit_notification(__event_emitter__, error_message, "error")
|
||||
return {
|
||||
"messages": [{"role": "assistant", "content": f"❌ {error_message}"}]
|
||||
}
|
||||
|
||||
parts = re.split(r"```html.*?```", messages[-1]["content"], flags=re.DOTALL)
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"User"
|
||||
if role == "user"
|
||||
else "Assistant" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} Message {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
error_message = "Unable to retrieve valid user message content."
|
||||
await self._emit_notification(__event_emitter__, error_message, "error")
|
||||
return {
|
||||
"messages": [{"role": "assistant", "content": f"❌ {error_message}"}]
|
||||
}
|
||||
|
||||
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
parts = re.split(r"```html.*?```", original_content, flags=re.DOTALL)
|
||||
long_text_content = ""
|
||||
if parts:
|
||||
for part in reversed(parts):
|
||||
@@ -564,7 +601,7 @@ class Action:
|
||||
break
|
||||
|
||||
if not long_text_content:
|
||||
long_text_content = messages[-1]["content"].strip()
|
||||
long_text_content = original_content.strip()
|
||||
|
||||
if len(long_text_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
short_text_message = f"Text content is too short ({len(long_text_content)} characters), unable to perform effective analysis. Please provide at least {self.valves.MIN_TEXT_LENGTH} characters of text."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
title: 智绘心图
|
||||
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iY3VycmVudENvbG9yIi8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iOSIgeDI9IjEyIiB5Mj0iNCIvPgogIDxjaXJjbGUgY3g9IjEyIiBjeT0iMyIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEyIiB5MT0iMTUiIHgyPSIxMiIgeTI9IjIwIi8+CiAgPGNpcmNsZSBjeD0iMTIiIGN5PSIyMSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjkiIHkxPSIxMiIgeDI9IjQiIHkyPSIxMiIvPgogIDxjaXJjbGUgY3g9IjMiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjE1IiB5MT0iMTIiIHgyPSIyMCIgeTI9IjEyIi8+CiAgPGNpcmNsZSBjeD0iMjEiIGN5PSIxMiIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEwLjUiIHkxPSIxMC41IiB4Mj0iNiIgeTI9IjYiLz4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iNSIgcj0iMS41Ii8+CiAgPGxpbmUgeDE9IjEzLjUiIHkxPSIxMC41IiB4Mj0iMTgiIHkyPSI2Ii8+CiAgPGNpcmNsZSBjeD0iMTkiIGN5PSI1IiByPSIxLjUiLz4KICA8bGluZSB4MT0iMTAuNSIgeTE9IjEzLjUiIHgyPSI2IiB5Mj0iMTgiLz4KICA8Y2lyY2xlIGN4PSI1IiBjeT0iMTkiIHI9IjEuNSIvPgogIDxsaW5lIHgxPSIxMy41IiB5MT0iMTMuNSIgeDI9IjE4IiB5Mj0iMTgiLz4KICA8Y2lyY2xlIGN4PSIxOSIgY3k9IjE5IiByPSIxLjUiLz4KPC9zdmc+
|
||||
version: 0.7.2
|
||||
version: 0.7.4
|
||||
description: 智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。
|
||||
"""
|
||||
|
||||
@@ -83,15 +83,12 @@ HTML_WRAPPER_TEMPLATE = """
|
||||
.plugin-item {
|
||||
flex: 1 1 400px; /* 默认宽度,允许伸缩 */
|
||||
min-width: 300px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.plugin-item:hover {
|
||||
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.plugin-item { flex: 1 1 100%; }
|
||||
@@ -128,7 +125,6 @@ CSS_TEMPLATE_MINDMAP = """
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--card-bg-color);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
height: 100%;
|
||||
@@ -169,7 +165,6 @@ CSS_TEMPLATE_MINDMAP = """
|
||||
background-size: 20px 20px;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -287,7 +282,8 @@ SCRIPT_TEMPLATE_MINDMAP = """
|
||||
try {
|
||||
const svgEl = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svgEl.style.width = '100%';
|
||||
svgEl.style.height = '500px';
|
||||
svgEl.style.height = 'auto';
|
||||
svgEl.style.minHeight = '300px';
|
||||
containerEl.innerHTML = '';
|
||||
containerEl.appendChild(svgEl);
|
||||
|
||||
@@ -409,6 +405,10 @@ class Action:
|
||||
default=False,
|
||||
description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="用于生成的最近消息数量。设置为1仅使用最后一条消息,更大值可包含更多上下文。",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -452,6 +452,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""从消息内容中提取文本,支持多模态消息格式"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# 多模态消息: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
@@ -541,18 +556,40 @@ class Action:
|
||||
)
|
||||
|
||||
messages = body.get("messages")
|
||||
if (
|
||||
not messages
|
||||
or not isinstance(messages, list)
|
||||
or not messages[-1].get("content")
|
||||
):
|
||||
if not messages or not isinstance(messages, list):
|
||||
error_message = "无法获取有效的用户消息内容。"
|
||||
await self._emit_notification(__event_emitter__, error_message, "error")
|
||||
return {
|
||||
"messages": [{"role": "assistant", "content": f"❌ {error_message}"}]
|
||||
}
|
||||
|
||||
parts = re.split(r"```html.*?```", messages[-1]["content"], flags=re.DOTALL)
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"用户"
|
||||
if role == "user"
|
||||
else "助手" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} 消息 {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
error_message = "无法获取有效的用户消息内容。"
|
||||
await self._emit_notification(__event_emitter__, error_message, "error")
|
||||
return {
|
||||
"messages": [{"role": "assistant", "content": f"❌ {error_message}"}]
|
||||
}
|
||||
|
||||
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
parts = re.split(r"```html.*?```", original_content, flags=re.DOTALL)
|
||||
long_text_content = ""
|
||||
if parts:
|
||||
for part in reversed(parts):
|
||||
@@ -561,7 +598,7 @@ class Action:
|
||||
break
|
||||
|
||||
if not long_text_content:
|
||||
long_text_content = messages[-1]["content"].strip()
|
||||
long_text_content = original_content.strip()
|
||||
|
||||
if len(long_text_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
short_text_message = f"文本内容过短({len(long_text_content)}字符),无法进行有效分析。请提供至少{self.valves.MIN_TEXT_LENGTH}字符的文本。"
|
||||
|
||||
@@ -325,6 +325,10 @@ class Action:
|
||||
default=False,
|
||||
description="Whether to force clear previous plugin results (if True, overwrites instead of merging).",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="Number of recent messages to use for generation. Set to 1 for just the last message, or higher for more context.",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -400,6 +404,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""Extract text from message content, supporting multimodal message formats"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# Multimodal message: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
@@ -492,10 +511,32 @@ class Action:
|
||||
original_content = ""
|
||||
try:
|
||||
messages = body.get("messages", [])
|
||||
if not messages or not messages[-1].get("content"):
|
||||
if not messages:
|
||||
raise ValueError("Unable to get valid user message content.")
|
||||
|
||||
original_content = messages[-1]["content"]
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"User"
|
||||
if role == "user"
|
||||
else "Assistant" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(
|
||||
f"[{role_label} Message {i}]\n{text_content}"
|
||||
)
|
||||
|
||||
if not aggregated_parts:
|
||||
raise ValueError("Unable to get valid user message content.")
|
||||
|
||||
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
if len(original_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
short_text_message = f"Text content too short ({len(original_content)} chars), recommended at least {self.valves.MIN_TEXT_LENGTH} chars for effective deep analysis.\n\n💡 Tip: For short texts, consider using '⚡ Flash Card' for quick refinement."
|
||||
|
||||
@@ -320,6 +320,10 @@ class Action:
|
||||
default=False,
|
||||
description="是否强制清除旧的插件结果(如果为 True,则不合并,直接覆盖)。",
|
||||
)
|
||||
MESSAGE_COUNT: int = Field(
|
||||
default=1,
|
||||
description="用于生成的最近消息数量。设置为1仅使用最后一条消息,更大值可包含更多上下文。",
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
self.valves = self.Valves()
|
||||
@@ -398,6 +402,21 @@ class Action:
|
||||
pattern = r"```html\s*<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?```"
|
||||
return re.sub(pattern, "", content).strip()
|
||||
|
||||
def _extract_text_content(self, content) -> str:
|
||||
"""从消息内容中提取文本,支持多模态消息格式"""
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# 多模态消息: [{"type": "text", "text": "..."}, {"type": "image_url", ...}]
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
elif isinstance(item, str):
|
||||
text_parts.append(item)
|
||||
return "\n".join(text_parts)
|
||||
return str(content) if content else ""
|
||||
|
||||
def _merge_html(
|
||||
self,
|
||||
existing_html_code: str,
|
||||
@@ -491,10 +510,30 @@ class Action:
|
||||
original_content = ""
|
||||
try:
|
||||
messages = body.get("messages", [])
|
||||
if not messages or not messages[-1].get("content"):
|
||||
if not messages:
|
||||
raise ValueError("无法获取有效的用户消息内容。")
|
||||
|
||||
original_content = messages[-1]["content"]
|
||||
# Get last N messages based on MESSAGE_COUNT
|
||||
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||
recent_messages = messages[-message_count:]
|
||||
|
||||
# Aggregate content from selected messages with labels
|
||||
aggregated_parts = []
|
||||
for i, msg in enumerate(recent_messages, 1):
|
||||
text_content = self._extract_text_content(msg.get("content"))
|
||||
if text_content:
|
||||
role = msg.get("role", "unknown")
|
||||
role_label = (
|
||||
"用户"
|
||||
if role == "user"
|
||||
else "助手" if role == "assistant" else role
|
||||
)
|
||||
aggregated_parts.append(f"[{role_label} 消息 {i}]\n{text_content}")
|
||||
|
||||
if not aggregated_parts:
|
||||
raise ValueError("无法获取有效的用户消息内容。")
|
||||
|
||||
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||
|
||||
if len(original_content) < self.valves.MIN_TEXT_LENGTH:
|
||||
short_text_message = f"文本内容过短({len(original_content)}字符),建议至少{self.valves.MIN_TEXT_LENGTH}字符以获得有效的深度分析。\n\n💡 提示:对于短文本,建议使用'⚡ 闪记卡'进行快速提炼。"
|
||||
|
||||
301
plugins/filters/context_enhancement_filter/README_CITATIONS.md
Normal file
301
plugins/filters/context_enhancement_filter/README_CITATIONS.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# Citations 处理功能文档
|
||||
|
||||
## 概述
|
||||
|
||||
`context_enhancement_filter.py` 现在支持自动处理模型响应中的 `citations` 和 `grounding_metadata`,将其转换为 Open WebUI 的标准引用格式,使搜索来源能够在聊天界面中正确展示。
|
||||
|
||||
## 支持的数据格式
|
||||
|
||||
### 1. Gemini Search 格式
|
||||
|
||||
模型响应包含顶层的 `citations` 数组和 `grounding_metadata`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "chatcmpl-xxx",
|
||||
"type": "stream",
|
||||
"model": "gemini-3-flash-preview-search",
|
||||
"content": "回答内容...",
|
||||
"citations": [
|
||||
{
|
||||
"source": {
|
||||
"url": "https://example.com/article",
|
||||
"name": "example.com"
|
||||
},
|
||||
"retrieved_at": "2025-12-27T17:50:03.472550"
|
||||
}
|
||||
],
|
||||
"grounding_metadata": {
|
||||
"web_search_queries": [
|
||||
"搜索查询1",
|
||||
"搜索查询2"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 嵌套在 messages 中的格式
|
||||
|
||||
Citations 数据嵌套在最后一条 assistant 消息中:
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "回答内容...",
|
||||
"citations": [...],
|
||||
"grounding_metadata": {...}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 自动引用提取
|
||||
|
||||
插件会自动从以下位置提取 citations:
|
||||
- 响应体顶层的 `citations` 字段
|
||||
- 最后一条 assistant 消息中的 `citations` 字段
|
||||
|
||||
### 2. 引用格式转换
|
||||
|
||||
将模型原始的 citations 格式转换为 Open WebUI 标准格式:
|
||||
|
||||
**输入格式:**
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"url": "https://example.com",
|
||||
"name": "example.com"
|
||||
},
|
||||
"retrieved_at": "2025-12-27T17:50:03.472550"
|
||||
}
|
||||
```
|
||||
|
||||
**输出格式:**
|
||||
```json
|
||||
{
|
||||
"type": "citation",
|
||||
"data": {
|
||||
"document": ["来源:example.com\n检索时间:2025-12-27T17:50:03.472550"],
|
||||
"metadata": [
|
||||
{
|
||||
"source": "example.com",
|
||||
"url": "https://example.com",
|
||||
"date_accessed": "2025-12-27T17:50:03.472550",
|
||||
"type": "web_search_result"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"name": "example.com",
|
||||
"url": "https://example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 搜索查询展示
|
||||
|
||||
如果响应包含 `grounding_metadata.web_search_queries`,会在界面上显示使用的搜索查询:
|
||||
|
||||
```
|
||||
🔍 使用了 4 个搜索查询
|
||||
```
|
||||
|
||||
### 4. 处理状态提示
|
||||
|
||||
成功处理 citations 后,会显示状态提示:
|
||||
|
||||
```
|
||||
✓ 已处理 7 个引用来源
|
||||
```
|
||||
|
||||
## 在 Open WebUI 中的展示效果
|
||||
|
||||
### 引用链接
|
||||
每个引用来源会在聊天界面中显示为可点击的链接,用户可以:
|
||||
- 点击查看原始来源
|
||||
- 查看检索时间等元数据
|
||||
- 了解信息的出处
|
||||
|
||||
### 搜索查询历史
|
||||
显示模型使用的搜索查询列表,帮助用户了解:
|
||||
- 模型如何理解和分解查询
|
||||
- 执行了哪些搜索操作
|
||||
- 搜索策略是否合理
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例 1:Gemini Search 模型
|
||||
|
||||
当使用支持搜索的 Gemini 模型时:
|
||||
|
||||
```python
|
||||
# 用户提问
|
||||
"MiniMax 最新的模型是什么?"
|
||||
|
||||
# 模型响应会包含 citations
|
||||
{
|
||||
"content": "MiniMax 最新的模型是 M2.1...",
|
||||
"citations": [
|
||||
{"source": {"url": "https://qbitai.com/...", "name": "qbitai.com"}, ...},
|
||||
{"source": {"url": "https://minimax.io/...", "name": "minimax.io"}, ...}
|
||||
]
|
||||
}
|
||||
|
||||
# Open WebUI 界面显示:
|
||||
# 1. 模型的回答内容
|
||||
# 2. 可点击的引用链接:qbitai.com, minimax.io 等
|
||||
# 3. 状态提示:✓ 已处理 7 个引用来源
|
||||
```
|
||||
|
||||
### 示例 2:自定义 Pipe/Filter
|
||||
|
||||
如果你的自定义 Pipe 或 Filter 返回包含 citations 的数据:
|
||||
|
||||
```python
|
||||
class CustomPipe:
|
||||
def pipe(self, body: dict) -> dict:
|
||||
# 执行搜索或检索操作
|
||||
search_results = self.perform_search(query)
|
||||
|
||||
# 构建响应,包含 citations
|
||||
return {
|
||||
"messages": [{
|
||||
"role": "assistant",
|
||||
"content": "基于搜索结果...",
|
||||
"citations": [
|
||||
{
|
||||
"source": {"url": url, "name": domain},
|
||||
"retrieved_at": datetime.now().isoformat()
|
||||
}
|
||||
for url, domain in search_results
|
||||
]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
`context_enhancement_filter` 会自动处理这些 citations 并在界面中展示。
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 启用 Filter
|
||||
|
||||
1. 在 Open WebUI 中安装 `context_enhancement_filter.py`
|
||||
2. 确保 Filter 已启用
|
||||
3. Citations 处理功能自动生效,无需额外配置
|
||||
|
||||
### 与其他功能的集成
|
||||
|
||||
Citations 处理与 Filter 的其他功能(环境变量注入、内容规范化等)无缝集成:
|
||||
|
||||
- **环境变量注入**:在 inlet 阶段处理
|
||||
- **内容规范化**:在 outlet 阶段处理
|
||||
- **Citations 处理**:在 outlet 阶段处理(与内容规范化并行)
|
||||
|
||||
## 技术实现细节
|
||||
|
||||
### 异步处理
|
||||
|
||||
Citations 处理使用异步方式,不会阻塞主响应流:
|
||||
|
||||
```python
|
||||
asyncio.create_task(self._process_citations(body, __event_emitter__))
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
|
||||
- 完善的异常捕获,确保 citations 处理失败不影响正常响应
|
||||
- 详细的日志记录,便于问题排查
|
||||
- 优雅降级:如果处理失败,用户仍能看到模型回答
|
||||
|
||||
### 兼容性
|
||||
|
||||
- 支持多种 citations 数据格式
|
||||
- 向后兼容:不包含 citations 的响应不受影响
|
||||
- 与 Open WebUI 原生 citations 系统完全兼容
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: Citations 没有显示?
|
||||
|
||||
**A:** 检查以下几点:
|
||||
1. 模型响应是否包含 `citations` 字段
|
||||
2. Filter 是否已启用
|
||||
3. `__event_emitter__` 是否正常工作
|
||||
4. 浏览器控制台是否有错误信息
|
||||
|
||||
### Q: 如何自定义 citations 展示格式?
|
||||
|
||||
**A:** 修改 `_process_citations` 方法中的 `document_text` 构建逻辑:
|
||||
|
||||
```python
|
||||
# 自定义文档文本格式
|
||||
document_text = f"""
|
||||
📄 **{source_name}**
|
||||
🔗 {source_url}
|
||||
⏰ {retrieved_at}
|
||||
"""
|
||||
```
|
||||
|
||||
### Q: 可以禁用 citations 处理吗?
|
||||
|
||||
**A:** 可以通过以下方式禁用:
|
||||
|
||||
1. **临时禁用**:在 outlet 方法中注释掉 citations 处理代码
|
||||
2. **条件禁用**:添加配置选项到 Valves:
|
||||
|
||||
```python
|
||||
class Valves(BaseModel):
|
||||
enable_citations_processing: bool = Field(
|
||||
default=True,
|
||||
description="启用 citations 自动处理"
|
||||
)
|
||||
```
|
||||
|
||||
## 与 websearch.py 的对比
|
||||
|
||||
| 特性 | websearch.py | context_enhancement_filter.py |
|
||||
|------|--------------|-------------------------------|
|
||||
| 功能 | 主动执行搜索 | 被动处理已有 citations |
|
||||
| 时机 | inlet (请求前) | outlet (响应后) |
|
||||
| 搜索引擎 | SearxNG | 依赖模型自身 |
|
||||
| 内容注入 | 直接修改用户消息 | 不修改消息 |
|
||||
| 引用展示 | ✅ 支持 | ✅ 支持 |
|
||||
|
||||
**推荐使用场景:**
|
||||
- 使用支持搜索的模型(如 Gemini Search):启用 `context_enhancement_filter`
|
||||
- 使用不支持搜索的模型:启用 `websearch.py`
|
||||
- 需要自定义搜索源:使用 `websearch.py` 配置 SearxNG
|
||||
|
||||
## 未来改进方向
|
||||
|
||||
1. **更丰富的元数据展示**
|
||||
- 显示网页标题、摘要
|
||||
- 展示相关性评分
|
||||
- 支持缩略图预览
|
||||
|
||||
2. **智能内容提取**
|
||||
- 从响应内容中提取被引用的具体段落
|
||||
- 高亮显示引用来源对应的文本
|
||||
|
||||
3. **引用验证**
|
||||
- 检查引用链接的有效性
|
||||
- 提供存档链接备份
|
||||
|
||||
4. **用户配置选项**
|
||||
- 自定义引用展示格式
|
||||
- 选择性显示/隐藏某些元数据
|
||||
- 引用分组和排序选项
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Open WebUI Plugin 开发文档](../../docs/features/plugin/tools/development.mdx)
|
||||
- [Event Emitter 使用指南](../../docs/features/plugin/development/events.mdx)
|
||||
- [WebSearch Filter 实现](../websearch/websearch.py)
|
||||
|
||||
## 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request 来改进这个功能!
|
||||
Reference in New Issue
Block a user