From 6de0d6fbe43252139b2d61f29acd121d378295fe Mon Sep 17 00:00:00 2001 From: Jeff fu Date: Mon, 5 Jan 2026 17:29:52 +0800 Subject: [PATCH] feat(infographic-markdown): add new plugin for JS render to Markdown - Add infographic_markdown.py (English) and infographic_markdown_cn.py (Chinese) - AI-powered infographic generator using AntV library - Renders SVG on frontend and embeds as Markdown Data URL image - Supports 18+ infographic templates (lists, charts, comparisons, etc.) Docs: - Add plugin README.md and README_CN.md - Add docs detail pages (infographic-markdown.md) - Update docs index pages with new plugin - Add 'JS Render to Markdown' pattern to plugin development guides - Update copilot-instructions.md with new advanced development pattern Version: 1.0.0 --- .github/copilot-instructions.md | 134 +++- docs/development/plugin-guide.md | 119 ++++ docs/development/plugin-guide.zh.md | 119 +++- docs/plugins/actions/index.md | 10 + docs/plugins/actions/index.zh.md | 10 + docs/plugins/actions/infographic-markdown.md | 120 ++++ .../actions/infographic-markdown.zh.md | 120 ++++ plugins/actions/js-render-poc/README.md | 170 +++++ plugins/actions/js-render-poc/README_CN.md | 174 +++++ .../js-render-poc/infographic_markdown.py | 592 ++++++++++++++++++ .../js-render-poc/infographic_markdown_cn.py | 592 ++++++++++++++++++ 11 files changed, 2155 insertions(+), 5 deletions(-) create mode 100644 docs/plugins/actions/infographic-markdown.md create mode 100644 docs/plugins/actions/infographic-markdown.zh.md create mode 100644 plugins/actions/js-render-poc/README.md create mode 100644 plugins/actions/js-render-poc/README_CN.md create mode 100644 plugins/actions/js-render-poc/infographic_markdown.py create mode 100644 plugins/actions/js-render-poc/infographic_markdown_cn.py diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0021d0c..0fc736c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -841,10 +841,136 @@ def add_math(paragraph, latex_str): omml = mathml2omml(mathml) # ... 插入 OMML 到 paragraph._element ... ``` - - [ ] 更新 `README.md` 插件列表 - - [ ] 更新 `README_CN.md` 插件列表 - - [ ] 更新/创建 `docs/` 下的对应文档 - - [ ] 确保文档版本号与代码一致 + +### JS 渲染并嵌入 Markdown (JS Render to Markdown) + +对于需要复杂前端渲染(如 AntV 图表、Mermaid 图表、ECharts)但希望结果**持久化为纯 Markdown 格式**的场景,推荐使用 Data URL 嵌入模式: + +#### 工作流程 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Plugin Workflow │ +├─────────────────────────────────────────────────────────────┤ +│ 1. Python Action │ +│ ├── 分析消息内容 │ +│ ├── 调用 LLM 生成结构化数据(可选) │ +│ └── 通过 __event_call__ 发送 JS 代码到前端 │ +├─────────────────────────────────────────────────────────────┤ +│ 2. Browser JS (via __event_call__) │ +│ ├── 动态加载可视化库(如 AntV、Mermaid) │ +│ ├── 离屏渲染 SVG/Canvas │ +│ ├── 使用 toDataURL() 导出 Base64 Data URL │ +│ └── 通过 REST API 更新消息内容 │ +├─────────────────────────────────────────────────────────────┤ +│ 3. Markdown 渲染 │ +│ └── 显示 ![描述](data:image/svg+xml;base64,...) │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 核心实现代码 + +**Python 端(发送 JS 执行):** + +```python +async def action(self, body, __event_call__, __metadata__, ...): + chat_id = self._extract_chat_id(body, __metadata__) + message_id = self._extract_message_id(body, __metadata__) + + # 生成 JS 代码 + js_code = self._generate_js_code( + chat_id=chat_id, + message_id=message_id, + data=processed_data, # 可视化所需数据 + ) + + # 执行 JS + if __event_call__: + await __event_call__({ + "type": "execute", + "data": {"code": js_code} + }) +``` + +**JavaScript 端(渲染并回写):** + +```javascript +(async function() { + // 1. 动态加载可视化库 + if (typeof VisualizationLib === 'undefined') { + await new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = 'https://cdn.example.com/lib.min.js'; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } + + // 2. 创建离屏容器 + const container = document.createElement('div'); + container.style.cssText = 'position:absolute;left:-9999px;'; + document.body.appendChild(container); + + // 3. 渲染可视化 + const instance = new VisualizationLib({ container, ... }); + instance.render(data); + + // 4. 导出为 Data URL + const dataUrl = await instance.toDataURL({ type: 'svg', embedResources: true }); + // 或手动转换 SVG: + // const svgData = new XMLSerializer().serializeToString(svgElement); + // const base64 = btoa(unescape(encodeURIComponent(svgData))); + // const dataUrl = "data:image/svg+xml;base64," + base64; + + // 5. 清理 + instance.destroy(); + document.body.removeChild(container); + + // 6. 生成 Markdown 图片 + const markdownImage = `![描述](${dataUrl})`; + + // 7. 通过 API 更新消息 + const token = localStorage.getItem("token"); + await fetch(`/api/v1/chats/${chatId}/messages/${messageId}/event`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + body: JSON.stringify({ + type: "chat:message", + data: { content: originalContent + "\n\n" + markdownImage } + }) + }); +})(); +``` + +#### 优势 + +- **纯 Markdown 输出**:结果是标准的 Markdown 图片语法,无需 HTML 代码块 +- **自包含**:图片以 Base64 Data URL 嵌入,无外部依赖 +- **持久化**:通过 API 回写,消息重新加载后图片仍然存在 +- **跨平台**:任何支持 Markdown 图片的客户端都能显示 +- **无服务端渲染依赖**:利用用户浏览器的渲染能力 + +#### 与 HTML 注入模式对比 + +| 特性 | HTML 注入 (`\`\`\`html`) | JS 渲染 + Markdown 图片 | +|------|-------------------------|------------------------| +| 输出格式 | HTML 代码块 | Markdown 图片 | +| 交互性 | ✅ 支持按钮、动画 | ❌ 静态图片 | +| 外部依赖 | 需要加载 JS 库 | 无(图片自包含) | +| 持久化 | 依赖浏览器渲染 | ✅ 永久可见 | +| 文件导出 | 需特殊处理 | ✅ 直接导出 | +| 适用场景 | 交互式内容 | 信息图、图表快照 | + +#### 参考实现 + +- `plugins/actions/js-render-poc/infographic_markdown.py` - AntV Infographic 生成并嵌入 +- `plugins/actions/js-render-poc/js_render_poc.py` - 基础概念验证 + + --- diff --git a/docs/development/plugin-guide.md b/docs/development/plugin-guide.md index 525c779..070a327 100644 --- a/docs/development/plugin-guide.md +++ b/docs/development/plugin-guide.md @@ -235,6 +235,125 @@ llm_response = await generate_chat_completion( ) ``` +### 4.4 JS Render to Markdown (Data URL Embedding) + +For scenarios requiring complex frontend rendering (e.g., AntV charts, Mermaid diagrams) but wanting **persistent pure Markdown output**, use the Data URL embedding pattern: + +#### Workflow + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 1. Python Action │ +│ ├── Analyze message content │ +│ ├── Call LLM to generate structured data (optional) │ +│ └── Send JS code to frontend via __event_call__ │ +├──────────────────────────────────────────────────────────────┤ +│ 2. Browser JS (via __event_call__) │ +│ ├── Dynamically load visualization library │ +│ ├── Render SVG/Canvas offscreen │ +│ ├── Export to Base64 Data URL via toDataURL() │ +│ └── Update message content via REST API │ +├──────────────────────────────────────────────────────────────┤ +│ 3. Markdown Rendering │ +│ └── Display ![description](data:image/svg+xml;base64,...) │ +└──────────────────────────────────────────────────────────────┘ +``` + +#### Python Side (Send JS for Execution) + +```python +async def action(self, body, __event_call__, __metadata__, ...): + chat_id = self._extract_chat_id(body, __metadata__) + message_id = self._extract_message_id(body, __metadata__) + + # Generate JS code + js_code = self._generate_js_code( + chat_id=chat_id, + message_id=message_id, + data=processed_data, + ) + + # Execute JS + if __event_call__: + await __event_call__({ + "type": "execute", + "data": {"code": js_code} + }) +``` + +#### JavaScript Side (Render and Write-back) + +```javascript +(async function() { + // 1. Load visualization library + if (typeof VisualizationLib === 'undefined') { + await new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = 'https://cdn.example.com/lib.min.js'; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } + + // 2. Create offscreen container + const container = document.createElement('div'); + container.style.cssText = 'position:absolute;left:-9999px;'; + document.body.appendChild(container); + + // 3. Render visualization + const instance = new VisualizationLib({ container }); + instance.render(data); + + // 4. Export to Data URL + const dataUrl = await instance.toDataURL({ type: 'svg', embedResources: true }); + + // 5. Cleanup + instance.destroy(); + document.body.removeChild(container); + + // 6. Generate Markdown image + const markdownImage = `![Chart](${dataUrl})`; + + // 7. Update message via API + const token = localStorage.getItem("token"); + await fetch(`/api/v1/chats/${chatId}/messages/${messageId}/event`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + body: JSON.stringify({ + type: "chat:message", + data: { content: originalContent + "\n\n" + markdownImage } + }) + }); +})(); +``` + +#### Benefits + +- **Pure Markdown Output**: Standard Markdown image syntax, no HTML code blocks +- **Self-Contained**: Images embedded as Base64 Data URL, no external dependencies +- **Persistent**: Via API write-back, images remain after page reload +- **Cross-Platform**: Works on any client supporting Markdown images + +#### HTML Injection vs JS Render to Markdown + +| Feature | HTML Injection | JS Render + Markdown | +|---------|----------------|----------------------| +| Output Format | HTML code block | Markdown image | +| Interactivity | ✅ Buttons, animations | ❌ Static image | +| External Deps | Requires JS libraries | None (self-contained) | +| Persistence | Depends on browser | ✅ Permanent | +| File Export | Needs special handling | ✅ Direct export | +| Use Case | Interactive content | Infographics, chart snapshots | + +#### Reference Implementations + +- `plugins/actions/js-render-poc/infographic_markdown.py` - AntV Infographic + Data URL +- `plugins/actions/js-render-poc/js_render_poc.py` - Basic proof of concept + --- ## 5. Best Practices & Design Principles diff --git a/docs/development/plugin-guide.zh.md b/docs/development/plugin-guide.zh.md index 74570fd..a127872 100644 --- a/docs/development/plugin-guide.zh.md +++ b/docs/development/plugin-guide.zh.md @@ -199,7 +199,124 @@ async def background_job(self, chat_id): pass ``` ---- +### 4.3 JS 渲染并嵌入 Markdown (Data URL 嵌入) + +对于需要复杂前端渲染(如 AntV 图表、Mermaid 图表)但希望结果**持久化为纯 Markdown 格式**的场景,推荐使用 Data URL 嵌入模式: + +#### 工作流程 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 1. Python Action │ +│ ├── 分析消息内容 │ +│ ├── 调用 LLM 生成结构化数据(可选) │ +│ └── 通过 __event_call__ 发送 JS 代码到前端 │ +├──────────────────────────────────────────────────────────────┤ +│ 2. Browser JS (通过 __event_call__) │ +│ ├── 动态加载可视化库 │ +│ ├── 离屏渲染 SVG/Canvas │ +│ ├── 使用 toDataURL() 导出 Base64 Data URL │ +│ └── 通过 REST API 更新消息内容 │ +├──────────────────────────────────────────────────────────────┤ +│ 3. Markdown 渲染 │ +│ └── 显示 ![描述](data:image/svg+xml;base64,...) │ +└──────────────────────────────────────────────────────────────┘ +``` + +#### Python 端(发送 JS 执行) + +```python +async def action(self, body, __event_call__, __metadata__, ...): + chat_id = self._extract_chat_id(body, __metadata__) + message_id = self._extract_message_id(body, __metadata__) + + # 生成 JS 代码 + js_code = self._generate_js_code( + chat_id=chat_id, + message_id=message_id, + data=processed_data, + ) + + # 执行 JS + if __event_call__: + await __event_call__({ + "type": "execute", + "data": {"code": js_code} + }) +``` + +#### JavaScript 端(渲染并回写) + +```javascript +(async function() { + // 1. 加载可视化库 + if (typeof VisualizationLib === 'undefined') { + await new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = 'https://cdn.example.com/lib.min.js'; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } + + // 2. 创建离屏容器 + const container = document.createElement('div'); + container.style.cssText = 'position:absolute;left:-9999px;'; + document.body.appendChild(container); + + // 3. 渲染可视化 + const instance = new VisualizationLib({ container }); + instance.render(data); + + // 4. 导出为 Data URL + const dataUrl = await instance.toDataURL({ type: 'svg', embedResources: true }); + + // 5. 清理 + instance.destroy(); + document.body.removeChild(container); + + // 6. 生成 Markdown 图片 + const markdownImage = `![图表](${dataUrl})`; + + // 7. 通过 API 更新消息 + const token = localStorage.getItem("token"); + await fetch(`/api/v1/chats/${chatId}/messages/${messageId}/event`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}` + }, + body: JSON.stringify({ + type: "chat:message", + data: { content: originalContent + "\n\n" + markdownImage } + }) + }); +})(); +``` + +#### 优势 + +- **纯 Markdown 输出**:结果是标准的 Markdown 图片语法,无需 HTML 代码块 +- **自包含**:图片以 Base64 Data URL 嵌入,无外部依赖 +- **持久化**:通过 API 回写,消息重新加载后图片仍然存在 +- **跨平台**:任何支持 Markdown 图片的客户端都能显示 + +#### HTML 注入 vs JS 渲染嵌入 Markdown + +| 特性 | HTML 注入 | JS 渲染 + Markdown 图片 | +|------|----------|------------------------| +| 输出格式 | HTML 代码块 | Markdown 图片 | +| 交互性 | ✅ 支持按钮、动画 | ❌ 静态图片 | +| 外部依赖 | 需要加载 JS 库 | 无(图片自包含) | +| 持久化 | 依赖浏览器渲染 | ✅ 永久可见 | +| 文件导出 | 需特殊处理 | ✅ 直接导出 | +| 适用场景 | 交互式内容 | 信息图、图表快照 | + +#### 参考实现 + +- `plugins/actions/js-render-poc/infographic_markdown.py` - AntV 信息图 + Data URL +- `plugins/actions/js-render-poc/js_render_poc.py` - 基础概念验证 ## 5. 最佳实践与设计原则 diff --git a/docs/plugins/actions/index.md b/docs/plugins/actions/index.md index b827425..f2d5d0b 100644 --- a/docs/plugins/actions/index.md +++ b/docs/plugins/actions/index.md @@ -77,6 +77,16 @@ Actions are interactive plugins that: [:octicons-arrow-right-24: Documentation](summary.md) +- :material-image-text:{ .lg .middle } **Infographic to Markdown** + + --- + + AI-powered infographic generator that renders SVG and embeds it as Markdown Data URL image. + + **Version:** 1.0.0 + + [:octicons-arrow-right-24: Documentation](infographic-markdown.md) + --- diff --git a/docs/plugins/actions/index.zh.md b/docs/plugins/actions/index.zh.md index f266383..f3e43db 100644 --- a/docs/plugins/actions/index.zh.md +++ b/docs/plugins/actions/index.zh.md @@ -77,6 +77,16 @@ Actions 是交互式插件,能够: [:octicons-arrow-right-24: 查看文档](summary.md) +- :material-image-text:{ .lg .middle } **信息图转 Markdown** + + --- + + AI 驱动的信息图生成器,渲染 SVG 并以 Markdown Data URL 图片嵌入。 + + **版本:** 1.0.0 + + [:octicons-arrow-right-24: 查看文档](infographic-markdown.zh.md) + --- diff --git a/docs/plugins/actions/infographic-markdown.md b/docs/plugins/actions/infographic-markdown.md new file mode 100644 index 0000000..beeda7b --- /dev/null +++ b/docs/plugins/actions/infographic-markdown.md @@ -0,0 +1,120 @@ +# Infographic to Markdown + +> **Version:** 1.0.0 | **Author:** Fu-Jie + +AI-powered infographic generator that renders SVG on the frontend and embeds it directly into Markdown as a Data URL image. + +## Overview + +This plugin combines the power of AI text analysis with AntV Infographic visualization to create beautiful infographics that are embedded directly into chat messages as Markdown images. + +### Key Features + +- :robot: **AI-Powered**: Automatically analyzes text and selects the best infographic template +- :bar_chart: **Multiple Templates**: Supports 18+ infographic templates (lists, charts, comparisons, etc.) +- :framed_picture: **Self-Contained**: SVG/PNG embedded as Data URL, no external dependencies +- :memo: **Markdown Native**: Results are pure Markdown images, compatible everywhere +- :arrows_counterclockwise: **API Writeback**: Updates message content via REST API for persistence + +### How It Works + +```mermaid +graph TD + A[User triggers action] --> B[Python extracts message content] + B --> C[LLM generates Infographic syntax] + C --> D[Frontend JS loads AntV library] + D --> E[Render SVG offscreen] + E --> F[Export to Data URL] + F --> G[Update message via API] + G --> H[Display as Markdown image] +``` + +## Installation + +1. Download `infographic_markdown.py` (English) or `infographic_markdown_cn.py` (Chinese) +2. Navigate to **Admin Panel** → **Settings** → **Functions** +3. Upload the file and configure settings +4. Use the action button in chat messages + +## Configuration + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `SHOW_STATUS` | bool | `true` | Show operation status updates | +| `MODEL_ID` | string | `""` | LLM model ID (empty = use current model) | +| `MIN_TEXT_LENGTH` | int | `50` | Minimum text length required | +| `MESSAGE_COUNT` | int | `1` | Number of recent messages to use | +| `SVG_WIDTH` | int | `800` | Width of generated SVG (pixels) | +| `EXPORT_FORMAT` | string | `"svg"` | Export format: `svg` or `png` | + +## Supported Templates + +| Category | Template | Description | +|----------|----------|-------------| +| List | `list-grid` | Grid cards | +| List | `list-vertical` | Vertical list | +| Tree | `tree-vertical` | Vertical tree | +| Tree | `tree-horizontal` | Horizontal tree | +| Mind Map | `mindmap` | Mind map | +| Process | `sequence-roadmap` | Roadmap | +| Process | `sequence-zigzag` | Zigzag process | +| Relation | `relation-sankey` | Sankey diagram | +| Relation | `relation-circle` | Circular relation | +| Compare | `compare-binary` | Binary comparison | +| Analysis | `compare-swot` | SWOT analysis | +| Quadrant | `quadrant-quarter` | Quadrant chart | +| Chart | `chart-bar` | Bar chart | +| Chart | `chart-column` | Column chart | +| Chart | `chart-line` | Line chart | +| Chart | `chart-pie` | Pie chart | +| Chart | `chart-doughnut` | Doughnut chart | +| Chart | `chart-area` | Area chart | + +## Usage Example + +1. Generate some text content in the chat (or have the AI generate it) +2. Click the **📊 Infographic to Markdown** action button +3. Wait for AI analysis and SVG rendering +4. The infographic will be embedded as a Markdown image + +## Technical Details + +### Data URL Embedding + +The plugin converts SVG graphics to Base64-encoded Data URLs: + +```javascript +const svgData = new XMLSerializer().serializeToString(svg); +const base64 = btoa(unescape(encodeURIComponent(svgData))); +const dataUri = "data:image/svg+xml;base64," + base64; +const markdownImage = `![description](${dataUri})`; +``` + +### AntV toDataURL API + +```javascript +// Export as SVG (recommended) +const svgUrl = await instance.toDataURL({ + type: 'svg', + embedResources: true +}); + +// Export as PNG +const pngUrl = await instance.toDataURL({ + type: 'png', + dpr: 2 +}); +``` + +## Notes + +1. **Browser Compatibility**: Requires modern browsers with ES6+ and Fetch API support +2. **Network Dependency**: First use requires loading AntV library from CDN +3. **Data URL Size**: Base64 encoding increases size by ~33% +4. **Chinese Fonts**: SVG export embeds fonts for correct display + +## Related Resources + +- [AntV Infographic Documentation](https://infographic.antv.vision/) +- [Infographic API Reference](https://infographic.antv.vision/reference/infographic-api) +- [Infographic Syntax Guide](https://infographic.antv.vision/learn/infographic-syntax) diff --git a/docs/plugins/actions/infographic-markdown.zh.md b/docs/plugins/actions/infographic-markdown.zh.md new file mode 100644 index 0000000..01c825f --- /dev/null +++ b/docs/plugins/actions/infographic-markdown.zh.md @@ -0,0 +1,120 @@ +# 信息图转 Markdown + +> **版本:** 1.0.0 | **作者:** Fu-Jie + +AI 驱动的信息图生成器,在前端渲染 SVG 并以 Data URL 图片格式直接嵌入到 Markdown 中。 + +## 概述 + +这个插件结合了 AI 文本分析能力和 AntV Infographic 可视化引擎,生成精美的信息图并以 Markdown 图片格式直接嵌入到聊天消息中。 + +### 主要特性 + +- :robot: **AI 驱动**: 自动分析文本并选择最佳的信息图模板 +- :bar_chart: **多种模板**: 支持 18+ 种信息图模板(列表、图表、对比等) +- :framed_picture: **自包含**: SVG/PNG 以 Data URL 嵌入,无外部依赖 +- :memo: **Markdown 原生**: 结果是纯 Markdown 图片,兼容任何平台 +- :arrows_counterclockwise: **API 回写**: 通过 REST API 更新消息内容实现持久化 + +### 工作原理 + +```mermaid +graph TD + A[用户触发动作] --> B[Python 提取消息内容] + B --> C[LLM 生成 Infographic 语法] + C --> D[前端 JS 加载 AntV 库] + D --> E[离屏渲染 SVG] + E --> F[导出为 Data URL] + F --> G[通过 API 更新消息] + G --> H[显示为 Markdown 图片] +``` + +## 安装 + +1. 下载 `infographic_markdown.py`(英文版)或 `infographic_markdown_cn.py`(中文版) +2. 进入 **管理面板** → **设置** → **功能** +3. 上传文件并配置设置 +4. 在聊天消息中使用动作按钮 + +## 配置选项 + +| 参数 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| `SHOW_STATUS` | bool | `true` | 是否显示操作状态 | +| `MODEL_ID` | string | `""` | LLM 模型 ID(空则使用当前模型) | +| `MIN_TEXT_LENGTH` | int | `50` | 最小文本长度要求 | +| `MESSAGE_COUNT` | int | `1` | 用于生成的最近消息数量 | +| `SVG_WIDTH` | int | `800` | 生成的 SVG 宽度(像素) | +| `EXPORT_FORMAT` | string | `"svg"` | 导出格式:`svg` 或 `png` | + +## 支持的模板 + +| 类别 | 模板名称 | 描述 | +|------|----------|------| +| 列表 | `list-grid` | 网格卡片 | +| 列表 | `list-vertical` | 垂直列表 | +| 树形 | `tree-vertical` | 垂直树 | +| 树形 | `tree-horizontal` | 水平树 | +| 思维导图 | `mindmap` | 思维导图 | +| 流程 | `sequence-roadmap` | 路线图 | +| 流程 | `sequence-zigzag` | 折线流程 | +| 关系 | `relation-sankey` | 桑基图 | +| 关系 | `relation-circle` | 圆形关系 | +| 对比 | `compare-binary` | 二元对比 | +| 分析 | `compare-swot` | SWOT 分析 | +| 象限 | `quadrant-quarter` | 四象限图 | +| 图表 | `chart-bar` | 条形图 | +| 图表 | `chart-column` | 柱状图 | +| 图表 | `chart-line` | 折线图 | +| 图表 | `chart-pie` | 饼图 | +| 图表 | `chart-doughnut` | 环形图 | +| 图表 | `chart-area` | 面积图 | + +## 使用示例 + +1. 在聊天中生成一些文本内容(或让 AI 生成) +2. 点击 **📊 信息图转 Markdown** 动作按钮 +3. 等待 AI 分析和 SVG 渲染 +4. 信息图将以 Markdown 图片形式嵌入 + +## 技术细节 + +### Data URL 嵌入 + +插件将 SVG 图形转换为 Base64 编码的 Data URL: + +```javascript +const svgData = new XMLSerializer().serializeToString(svg); +const base64 = btoa(unescape(encodeURIComponent(svgData))); +const dataUri = "data:image/svg+xml;base64," + base64; +const markdownImage = `![描述](${dataUri})`; +``` + +### AntV toDataURL API + +```javascript +// 导出 SVG(推荐) +const svgUrl = await instance.toDataURL({ + type: 'svg', + embedResources: true +}); + +// 导出 PNG +const pngUrl = await instance.toDataURL({ + type: 'png', + dpr: 2 +}); +``` + +## 注意事项 + +1. **浏览器兼容性**: 需要现代浏览器支持 ES6+ 和 Fetch API +2. **网络依赖**: 首次使用需要从 CDN 加载 AntV Infographic 库 +3. **Data URL 大小**: Base64 编码会增加约 33% 的体积 +4. **中文字体**: SVG 导出时会嵌入字体以确保正确显示 + +## 相关资源 + +- [AntV Infographic 官方文档](https://infographic.antv.vision/) +- [Infographic API 参考](https://infographic.antv.vision/reference/infographic-api) +- [Infographic 语法规范](https://infographic.antv.vision/learn/infographic-syntax) diff --git a/plugins/actions/js-render-poc/README.md b/plugins/actions/js-render-poc/README.md new file mode 100644 index 0000000..f749229 --- /dev/null +++ b/plugins/actions/js-render-poc/README.md @@ -0,0 +1,170 @@ +# Infographic to Markdown + +> **Version:** 1.0.0 + +AI-powered infographic generator that renders SVG on the frontend and embeds it directly into Markdown as a Data URL image. + +## Overview + +This plugin combines the power of AI text analysis with AntV Infographic visualization to create beautiful infographics that are embedded directly into chat messages as Markdown images. + +### How It Works + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Open WebUI Plugin │ +├─────────────────────────────────────────────────────────────┤ +│ 1. Python Action │ +│ ├── Receive message content │ +│ ├── Call LLM to generate Infographic syntax │ +│ └── Send __event_call__ to execute frontend JS │ +├─────────────────────────────────────────────────────────────┤ +│ 2. Browser JS (via __event_call__) │ +│ ├── Dynamically load AntV Infographic library │ +│ ├── Render SVG offscreen │ +│ ├── Export to Data URL via toDataURL() │ +│ └── Update message content via REST API │ +├─────────────────────────────────────────────────────────────┤ +│ 3. Markdown Rendering │ +│ └── Display ![description](data:image/svg+xml;base64,...) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Features + +- 🤖 **AI-Powered**: Automatically analyzes text and selects the best infographic template +- 📊 **Multiple Templates**: Supports 18+ infographic templates (lists, charts, comparisons, etc.) +- 🖼️ **Self-Contained**: SVG/PNG embedded as Data URL, no external dependencies +- 📝 **Markdown Native**: Results are pure Markdown images, compatible everywhere +- 🔄 **API Writeback**: Updates message content via REST API for persistence + +## Plugins in This Directory + +### 1. `infographic_markdown.py` - Main Plugin ⭐ +- **Purpose**: Production use +- **Features**: Full AI + AntV Infographic + Data URL embedding + +### 2. `js_render_poc.py` - Proof of Concept +- **Purpose**: Learning and testing +- **Features**: Simple SVG creation demo, `__event_call__` pattern + +## Configuration (Valves) + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `SHOW_STATUS` | bool | `true` | Show operation status updates | +| `MODEL_ID` | string | `""` | LLM model ID (empty = use current model) | +| `MIN_TEXT_LENGTH` | int | `50` | Minimum text length required | +| `MESSAGE_COUNT` | int | `1` | Number of recent messages to use | +| `SVG_WIDTH` | int | `800` | Width of generated SVG (pixels) | +| `EXPORT_FORMAT` | string | `"svg"` | Export format: `svg` or `png` | + +## Supported Templates + +| Category | Template | Description | +|----------|----------|-------------| +| List | `list-grid` | Grid cards | +| List | `list-vertical` | Vertical list | +| Tree | `tree-vertical` | Vertical tree | +| Tree | `tree-horizontal` | Horizontal tree | +| Mind Map | `mindmap` | Mind map | +| Process | `sequence-roadmap` | Roadmap | +| Process | `sequence-zigzag` | Zigzag process | +| Relation | `relation-sankey` | Sankey diagram | +| Relation | `relation-circle` | Circular relation | +| Compare | `compare-binary` | Binary comparison | +| Analysis | `compare-swot` | SWOT analysis | +| Quadrant | `quadrant-quarter` | Quadrant chart | +| Chart | `chart-bar` | Bar chart | +| Chart | `chart-column` | Column chart | +| Chart | `chart-line` | Line chart | +| Chart | `chart-pie` | Pie chart | +| Chart | `chart-doughnut` | Doughnut chart | +| Chart | `chart-area` | Area chart | + +## Syntax Examples + +### Grid List +```infographic +infographic list-grid +data + title Project Overview + items + - label Module A + desc Description of module A + - label Module B + desc Description of module B +``` + +### Binary Comparison +```infographic +infographic compare-binary +data + title Pros vs Cons + items + - label Pros + children + - label Strong R&D + desc Technology leadership + - label Cons + children + - label Weak brand + desc Insufficient marketing +``` + +### Bar Chart +```infographic +infographic chart-bar +data + title Quarterly Revenue + items + - label Q1 + value 120 + - label Q2 + value 150 +``` + +## Technical Details + +### Data URL Embedding +```javascript +// SVG to Base64 Data URL +const svgData = new XMLSerializer().serializeToString(svg); +const base64 = btoa(unescape(encodeURIComponent(svgData))); +const dataUri = "data:image/svg+xml;base64," + base64; + +// Markdown image syntax +const markdownImage = `![description](${dataUri})`; +``` + +### AntV toDataURL API +```javascript +// Export as SVG (recommended, supports embedded resources) +const svgUrl = await instance.toDataURL({ + type: 'svg', + embedResources: true +}); + +// Export as PNG (more compatible but larger) +const pngUrl = await instance.toDataURL({ + type: 'png', + dpr: 2 +}); +``` + +## Notes + +1. **Browser Compatibility**: Requires modern browsers with ES6+ and Fetch API support +2. **Network Dependency**: First use requires loading AntV library from CDN +3. **Data URL Size**: Base64 encoding increases size by ~33% +4. **Chinese Fonts**: SVG export embeds fonts for correct display + +## Related Resources + +- [AntV Infographic Documentation](https://infographic.antv.vision/) +- [Infographic API Reference](https://infographic.antv.vision/reference/infographic-api) +- [Infographic Syntax Guide](https://infographic.antv.vision/learn/infographic-syntax) + +## License + +MIT License diff --git a/plugins/actions/js-render-poc/README_CN.md b/plugins/actions/js-render-poc/README_CN.md new file mode 100644 index 0000000..62e7496 --- /dev/null +++ b/plugins/actions/js-render-poc/README_CN.md @@ -0,0 +1,174 @@ +# 信息图转 Markdown + +> **版本:** 1.0.0 + +AI 驱动的信息图生成器,在前端渲染 SVG 并以 Data URL 图片格式直接嵌入到 Markdown 中。 + +## 概述 + +这个插件结合了 AI 文本分析能力和 AntV Infographic 可视化引擎,生成精美的信息图并以 Markdown 图片格式直接嵌入到聊天消息中。 + +### 工作原理 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Open WebUI 插件 │ +├─────────────────────────────────────────────────────────────┤ +│ 1. Python Action │ +│ ├── 接收消息内容 │ +│ ├── 调用 LLM 生成 Infographic 语法 │ +│ └── 发送 __event_call__ 执行前端 JS │ +├─────────────────────────────────────────────────────────────┤ +│ 2. 浏览器 JS (通过 __event_call__) │ +│ ├── 动态加载 AntV Infographic 库 │ +│ ├── 离屏渲染 SVG │ +│ ├── 使用 toDataURL() 导出 Data URL │ +│ └── 通过 REST API 更新消息内容 │ +├─────────────────────────────────────────────────────────────┤ +│ 3. Markdown 渲染 │ +│ └── 显示 ![描述](data:image/svg+xml;base64,...) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 功能特点 + +- 🤖 **AI 驱动**: 自动分析文本并选择最佳的信息图模板 +- 📊 **多种模板**: 支持 18+ 种信息图模板(列表、图表、对比等) +- 🖼️ **自包含**: SVG/PNG 以 Data URL 嵌入,无外部依赖 +- 📝 **Markdown 原生**: 结果是纯 Markdown 图片,兼容任何平台 +- 🔄 **API 回写**: 通过 REST API 更新消息内容实现持久化 + +## 目录中的插件 + +### 1. `infographic_markdown.py` - 主插件 ⭐ +- **用途**: 生产使用 +- **功能**: 完整的 AI + AntV Infographic + Data URL 嵌入 + +### 2. `infographic_markdown_cn.py` - 主插件(中文版) +- **用途**: 生产使用 +- **功能**: 与英文版相同,界面文字为中文 + +### 3. `js_render_poc.py` - 概念验证 +- **用途**: 学习和测试 +- **功能**: 简单的 SVG 创建演示,`__event_call__` 模式 + +## 配置选项 (Valves) + +| 参数 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| `SHOW_STATUS` | bool | `true` | 是否显示操作状态 | +| `MODEL_ID` | string | `""` | LLM 模型 ID(空则使用当前模型) | +| `MIN_TEXT_LENGTH` | int | `50` | 最小文本长度要求 | +| `MESSAGE_COUNT` | int | `1` | 用于生成的最近消息数量 | +| `SVG_WIDTH` | int | `800` | 生成的 SVG 宽度(像素) | +| `EXPORT_FORMAT` | string | `"svg"` | 导出格式:`svg` 或 `png` | + +## 支持的模板 + +| 类别 | 模板名称 | 描述 | +|------|----------|------| +| 列表 | `list-grid` | 网格卡片 | +| 列表 | `list-vertical` | 垂直列表 | +| 树形 | `tree-vertical` | 垂直树 | +| 树形 | `tree-horizontal` | 水平树 | +| 思维导图 | `mindmap` | 思维导图 | +| 流程 | `sequence-roadmap` | 路线图 | +| 流程 | `sequence-zigzag` | 折线流程 | +| 关系 | `relation-sankey` | 桑基图 | +| 关系 | `relation-circle` | 圆形关系 | +| 对比 | `compare-binary` | 二元对比 | +| 分析 | `compare-swot` | SWOT 分析 | +| 象限 | `quadrant-quarter` | 四象限图 | +| 图表 | `chart-bar` | 条形图 | +| 图表 | `chart-column` | 柱状图 | +| 图表 | `chart-line` | 折线图 | +| 图表 | `chart-pie` | 饼图 | +| 图表 | `chart-doughnut` | 环形图 | +| 图表 | `chart-area` | 面积图 | + +## 语法示例 + +### 网格列表 +```infographic +infographic list-grid +data + title 项目概览 + items + - label 模块一 + desc 这是第一个模块的描述 + - label 模块二 + desc 这是第二个模块的描述 +``` + +### 二元对比 +```infographic +infographic compare-binary +data + title 优劣对比 + items + - label 优势 + children + - label 研发能力强 + desc 技术领先 + - label 劣势 + children + - label 品牌曝光不足 + desc 营销力度不够 +``` + +### 条形图 +```infographic +infographic chart-bar +data + title 季度收入 + items + - label Q1 + value 120 + - label Q2 + value 150 +``` + +## 技术细节 + +### Data URL 嵌入 +```javascript +// SVG 转 Base64 Data URL +const svgData = new XMLSerializer().serializeToString(svg); +const base64 = btoa(unescape(encodeURIComponent(svgData))); +const dataUri = "data:image/svg+xml;base64," + base64; + +// Markdown 图片语法 +const markdownImage = `![描述](${dataUri})`; +``` + +### AntV toDataURL API +```javascript +// 导出 SVG(推荐,支持嵌入资源) +const svgUrl = await instance.toDataURL({ + type: 'svg', + embedResources: true +}); + +// 导出 PNG(更兼容但体积更大) +const pngUrl = await instance.toDataURL({ + type: 'png', + dpr: 2 +}); +``` + +## 注意事项 + +1. **浏览器兼容性**: 需要现代浏览器支持 ES6+ 和 Fetch API +2. **网络依赖**: 首次使用需要从 CDN 加载 AntV Infographic 库 +3. **Data URL 大小**: Base64 编码会增加约 33% 的体积 +4. **中文字体**: SVG 导出时会嵌入字体以确保正确显示 + +## 相关资源 + +- [AntV Infographic 官方文档](https://infographic.antv.vision/) +- [Infographic API 参考](https://infographic.antv.vision/reference/infographic-api) +- [Infographic 语法规范](https://infographic.antv.vision/learn/infographic-syntax) + +## 许可证 + +MIT License diff --git a/plugins/actions/js-render-poc/infographic_markdown.py b/plugins/actions/js-render-poc/infographic_markdown.py new file mode 100644 index 0000000..2a66e63 --- /dev/null +++ b/plugins/actions/js-render-poc/infographic_markdown.py @@ -0,0 +1,592 @@ +""" +title: 📊 Infographic to Markdown +author: Fu-Jie +version: 1.0.0 +description: AI生成信息图语法,前端渲染SVG并转换为Markdown图片格式嵌入消息。支持AntV Infographic模板。 +""" + +import time +import json +import logging +import re +from typing import Optional, Callable, Awaitable, Any, Dict +from pydantic import BaseModel, Field +from fastapi import Request +from datetime import datetime + +from open_webui.utils.chat import generate_chat_completion +from open_webui.models.users import Users + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# ================================================================= +# LLM Prompts +# ================================================================= + +SYSTEM_PROMPT_INFOGRAPHIC = """ +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 ` +- 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 + +Choose the most appropriate template based on the content structure: + +#### 1. List & Hierarchy +- **List**: `list-grid` (Grid Cards), `list-vertical` (Vertical List) +- **Tree**: `tree-vertical` (Vertical Tree), `tree-horizontal` (Horizontal Tree) +- **Mindmap**: `mindmap` (Mind Map) + +#### 2. Sequence & Relationship +- **Process**: `sequence-roadmap` (Roadmap), `sequence-zigzag` (Zigzag Process) +- **Relationship**: `relation-sankey` (Sankey Diagram), `relation-circle` (Circular) + +#### 3. Comparison & Analysis +- **Comparison**: `compare-binary` (Binary Comparison) +- **Analysis**: `compare-swot` (SWOT Analysis), `quadrant-quarter` (Quadrant Chart) + +#### 4. Charts & Data +- **Charts**: `chart-bar`, `chart-column`, `chart-line`, `chart-pie`, `chart-doughnut`, `chart-area` + +### Data Structure Examples + +#### A. Standard List/Tree +```infographic +infographic list-grid +data + title Project Modules + items + - label Module A + desc Description of A + - label Module B + desc Description of B +``` + +#### B. Binary Comparison +```infographic +infographic compare-binary +data + title Advantages vs Disadvantages + items + - label Advantages + children + - label Strong R&D + desc Leading technology + - label Disadvantages + children + - label Weak brand + desc Insufficient marketing +``` + +#### C. Charts +```infographic +infographic chart-bar +data + title Quarterly Revenue + items + - label Q1 + value 120 + - label Q2 + value 150 +``` + +### Common Data Fields +- `label`: Main title/label (Required) +- `desc`: Description text (max 30 Chinese chars / 60 English chars for `list-grid`) +- `value`: Numeric value (for charts) +- `children`: Nested items + +## Output Requirements +1. **Language**: Output content in the user's language. +2. **Format**: Wrap output in ```infographic ... ```. +3. **No Colons**: Do NOT use colons after keys. +4. **Indentation**: Use 2 spaces. +""" + +USER_PROMPT_GENERATE = """ +Please analyze the following text content and convert its core information into AntV Infographic syntax format. + +--- +**User Context:** +User Name: {user_name} +Current Date/Time: {current_date_time_str} +User Language: {user_language} +--- + +**Text Content:** +{long_text_content} + +Please select the most appropriate infographic template based on text characteristics and output standard infographic syntax. + +**Important Note:** +- If using `list-grid` format, ensure each card's `desc` description is limited to **maximum 30 Chinese characters** (or **approximately 60 English characters**). +- Descriptions should be concise and highlight key points. +""" + + +class Action: + class Valves(BaseModel): + SHOW_STATUS: bool = Field( + default=True, description="Show operation status updates in chat interface." + ) + MODEL_ID: str = Field( + default="", + description="LLM model ID for text analysis. If empty, uses current conversation model.", + ) + MIN_TEXT_LENGTH: int = Field( + default=50, + description="Minimum text length (characters) required for infographic analysis.", + ) + MESSAGE_COUNT: int = Field( + default=1, + description="Number of recent messages to use for generation.", + ) + SVG_WIDTH: int = Field( + default=800, + description="Width of generated SVG in pixels.", + ) + EXPORT_FORMAT: str = Field( + default="svg", + description="Export format: 'svg' or 'png'.", + ) + + def __init__(self): + self.valves = self.Valves() + + def _extract_chat_id(self, body: dict, metadata: Optional[dict]) -> str: + """Extract chat_id from body or metadata""" + if isinstance(body, dict): + chat_id = body.get("chat_id") + if isinstance(chat_id, str) and chat_id.strip(): + return chat_id.strip() + + body_metadata = body.get("metadata", {}) + if isinstance(body_metadata, dict): + chat_id = body_metadata.get("chat_id") + if isinstance(chat_id, str) and chat_id.strip(): + return chat_id.strip() + + if isinstance(metadata, dict): + chat_id = metadata.get("chat_id") + if isinstance(chat_id, str) and chat_id.strip(): + return chat_id.strip() + + return "" + + def _extract_message_id(self, body: dict, metadata: Optional[dict]) -> str: + """Extract message_id from body or metadata""" + if isinstance(body, dict): + message_id = body.get("id") + if isinstance(message_id, str) and message_id.strip(): + return message_id.strip() + + body_metadata = body.get("metadata", {}) + if isinstance(body_metadata, dict): + message_id = body_metadata.get("message_id") + if isinstance(message_id, str) and message_id.strip(): + return message_id.strip() + + if isinstance(metadata, dict): + message_id = metadata.get("message_id") + if isinstance(message_id, str) and message_id.strip(): + return message_id.strip() + + return "" + + def _extract_infographic_syntax(self, llm_output: str) -> str: + """Extract infographic syntax from LLM output""" + match = re.search(r"```infographic\s*(.*?)\s*```", llm_output, re.DOTALL) + if match: + return match.group(1).strip() + else: + logger.warning("LLM output did not follow expected format, treating entire output as syntax.") + return llm_output.strip() + + def _extract_text_content(self, content) -> str: + """Extract text from message content, supporting multimodal formats""" + if isinstance(content, str): + return content + elif isinstance(content, list): + 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 "" + + async def _emit_status(self, emitter, description: str, done: bool = False): + """Send status update event""" + if self.valves.SHOW_STATUS and emitter: + await emitter( + {"type": "status", "data": {"description": description, "done": done}} + ) + + def _generate_js_code( + self, + unique_id: str, + chat_id: str, + message_id: str, + infographic_syntax: str, + svg_width: int, + export_format: str, + ) -> str: + """Generate JavaScript code for frontend SVG rendering""" + + # Escape the syntax for JS embedding + syntax_escaped = ( + infographic_syntax + .replace("\\", "\\\\") + .replace("`", "\\`") + .replace("${", "\\${") + .replace("", "<\\/script>") + ) + + # Template mapping (same as infographic.py) + template_mapping_js = """ + 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', + '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' + }; + """ + + return f""" +(async function() {{ + const uniqueId = "{unique_id}"; + const chatId = "{chat_id}"; + const messageId = "{message_id}"; + const svgWidth = {svg_width}; + const exportFormat = "{export_format}"; + + console.log("[Infographic Markdown] Starting render..."); + console.log("[Infographic Markdown] chatId:", chatId, "messageId:", messageId); + + try {{ + // Load AntV Infographic if not loaded + if (typeof AntVInfographic === 'undefined') {{ + console.log("[Infographic Markdown] Loading AntV Infographic library..."); + await new Promise((resolve, reject) => {{ + const script = document.createElement('script'); + script.src = 'https://unpkg.com/@antv/infographic@latest/dist/infographic.min.js'; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }}); + console.log("[Infographic Markdown] Library loaded."); + }} + + const {{ Infographic }} = AntVInfographic; + + // Get infographic syntax + let syntaxContent = `{syntax_escaped}`; + console.log("[Infographic Markdown] Original syntax:", syntaxContent.substring(0, 200) + "..."); + + // Clean up syntax + const backtick = String.fromCharCode(96); + const prefix = backtick + backtick + backtick + 'infographic'; + const simplePrefix = backtick + backtick + backtick; + + if (syntaxContent.toLowerCase().startsWith(prefix)) {{ + syntaxContent = syntaxContent.substring(prefix.length).trim(); + }} else if (syntaxContent.startsWith(simplePrefix)) {{ + syntaxContent = syntaxContent.substring(simplePrefix.length).trim(); + }} + + if (syntaxContent.endsWith(simplePrefix)) {{ + syntaxContent = syntaxContent.substring(0, syntaxContent.length - simplePrefix.length).trim(); + }} + + // Fix colons after keywords + syntaxContent = syntaxContent.replace(/^(data|items|children|theme|config):/gm, '$1'); + syntaxContent = syntaxContent.replace(/(\\s)(children|items):/g, '$1$2'); + + // Ensure infographic prefix + if (!syntaxContent.trim().toLowerCase().startsWith('infographic')) {{ + syntaxContent = 'infographic list-grid\\n' + syntaxContent; + }} + + // Apply template mapping + {template_mapping_js} + + for (const [key, value] of Object.entries(TEMPLATE_MAPPING)) {{ + const regex = new RegExp(`infographic\\\\s+${{key}}(?=\\\\s|$)`, 'i'); + if (regex.test(syntaxContent)) {{ + console.log(`[Infographic Markdown] Auto-mapping: ${{key}} -> ${{value}}`); + syntaxContent = syntaxContent.replace(regex, `infographic ${{value}}`); + break; + }} + }} + + console.log("[Infographic Markdown] Cleaned syntax:", syntaxContent.substring(0, 200) + "..."); + + // Create offscreen container + const container = document.createElement('div'); + container.id = 'infographic-offscreen-' + uniqueId; + container.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:' + svgWidth + 'px;'; + document.body.appendChild(container); + + // Create and render infographic + const instance = new Infographic({{ + container: '#' + container.id, + width: svgWidth, + padding: 24, + }}); + + console.log("[Infographic Markdown] Rendering infographic..."); + instance.render(syntaxContent); + + // Wait for render and export + await new Promise(resolve => setTimeout(resolve, 1000)); + + let dataUrl; + if (exportFormat === 'png') {{ + dataUrl = await instance.toDataURL({{ type: 'png', dpr: 2 }}); + }} else {{ + dataUrl = await instance.toDataURL({{ type: 'svg', embedResources: true }}); + }} + + console.log("[Infographic Markdown] Data URL generated, length:", dataUrl.length); + + // Cleanup + instance.destroy(); + document.body.removeChild(container); + + // Generate markdown image + const markdownImage = `![📊 AI 生成的信息图](${{dataUrl}})`; + + // Update message via API + if (chatId && messageId) {{ + const token = localStorage.getItem("token"); + + // Get current message content + const getResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{ + method: "GET", + headers: {{ "Authorization": `Bearer ${{token}}` }} + }}); + + if (!getResponse.ok) {{ + throw new Error("Failed to get chat data: " + getResponse.status); + }} + + const chatData = await getResponse.json(); + let originalContent = ""; + + if (chatData.chat && chatData.chat.messages) {{ + const targetMsg = chatData.chat.messages.find(m => m.id === messageId); + if (targetMsg && targetMsg.content) {{ + originalContent = targetMsg.content; + }} + }} + + // Remove existing infographic images + const infographicPattern = /\\n*!\\[📊[^\\]]*\\]\\(data:image\\/[^)]+\\)/g; + let cleanedContent = originalContent.replace(infographicPattern, ""); + cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim(); + + // Append new image + const newContent = cleanedContent + "\\n\\n" + markdownImage; + + // Update message + const updateResponse = await fetch(`/api/v1/chats/${{chatId}}/messages/${{messageId}}/event`, {{ + method: "POST", + headers: {{ + "Content-Type": "application/json", + "Authorization": `Bearer ${{token}}` + }}, + body: JSON.stringify({{ + type: "chat:message", + data: {{ content: newContent }} + }}) + }}); + + if (updateResponse.ok) {{ + console.log("[Infographic Markdown] ✅ Message updated successfully!"); + }} else {{ + console.error("[Infographic Markdown] API error:", updateResponse.status); + }} + }} else {{ + console.warn("[Infographic Markdown] ⚠️ Missing chatId or messageId"); + }} + + }} catch (error) {{ + console.error("[Infographic Markdown] Error:", error); + }} +}})(); +""" + + async def action( + self, + body: dict, + __user__: dict = None, + __event_emitter__=None, + __event_call__: Optional[Callable[[Any], Awaitable[None]]] = None, + __metadata__: Optional[dict] = None, + __request__: Request = None, + ) -> dict: + """ + Generate infographic using AntV and embed as Markdown image. + """ + logger.info("Action: Infographic to Markdown started") + + # Get user information + if isinstance(__user__, (list, tuple)): + user_language = __user__[0].get("language", "en") if __user__ else "en" + user_name = __user__[0].get("name", "User") if __user__[0] else "User" + user_id = __user__[0].get("id", "unknown_user") if __user__ else "unknown_user" + elif isinstance(__user__, dict): + user_language = __user__.get("language", "en") + user_name = __user__.get("name", "User") + user_id = __user__.get("id", "unknown_user") + else: + user_language = "en" + user_name = "User" + user_id = "unknown_user" + + # Get current time + now = datetime.now() + current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S") + + try: + messages = body.get("messages", []) + if not messages: + raise ValueError("No messages available.") + + # Get recent messages + message_count = min(self.valves.MESSAGE_COUNT, len(messages)) + recent_messages = messages[-message_count:] + + # Aggregate content + aggregated_parts = [] + for msg in recent_messages: + text_content = self._extract_text_content(msg.get("content")) + if text_content: + aggregated_parts.append(text_content) + + if not aggregated_parts: + raise ValueError("No text content found in messages.") + + long_text_content = "\n\n---\n\n".join(aggregated_parts) + + # Remove existing HTML blocks + parts = re.split(r"```html.*?```", long_text_content, flags=re.DOTALL) + clean_content = "" + for part in reversed(parts): + if part.strip(): + clean_content = part.strip() + break + + if not clean_content: + clean_content = long_text_content.strip() + + # Check minimum length + if len(clean_content) < self.valves.MIN_TEXT_LENGTH: + await self._emit_status( + __event_emitter__, + f"⚠️ 内容太短 ({len(clean_content)} 字符),至少需要 {self.valves.MIN_TEXT_LENGTH} 字符", + True, + ) + return body + + await self._emit_status(__event_emitter__, "📊 正在分析内容...", False) + + # Generate infographic syntax via LLM + formatted_user_prompt = USER_PROMPT_GENERATE.format( + user_name=user_name, + current_date_time_str=current_date_time_str, + user_language=user_language, + long_text_content=clean_content, + ) + + target_model = self.valves.MODEL_ID or body.get("model") + + llm_payload = { + "model": target_model, + "messages": [ + {"role": "system", "content": SYSTEM_PROMPT_INFOGRAPHIC}, + {"role": "user", "content": formatted_user_prompt}, + ], + "stream": False, + } + + user_obj = Users.get_user_by_id(user_id) + if not user_obj: + raise ValueError(f"Unable to get user object: {user_id}") + + await self._emit_status(__event_emitter__, "📊 AI 正在生成信息图语法...", False) + + llm_response = await generate_chat_completion(__request__, llm_payload, user_obj) + + if not llm_response or "choices" not in llm_response or not llm_response["choices"]: + raise ValueError("Invalid LLM response.") + + assistant_content = llm_response["choices"][0]["message"]["content"] + infographic_syntax = self._extract_infographic_syntax(assistant_content) + + logger.info(f"Generated syntax: {infographic_syntax[:200]}...") + + # Extract IDs for API callback + chat_id = self._extract_chat_id(body, __metadata__) + message_id = self._extract_message_id(body, __metadata__) + unique_id = f"ig_{int(time.time() * 1000)}" + + await self._emit_status(__event_emitter__, "📊 正在渲染 SVG...", False) + + # Execute JS to render and embed + if __event_call__: + js_code = self._generate_js_code( + unique_id=unique_id, + chat_id=chat_id, + message_id=message_id, + infographic_syntax=infographic_syntax, + svg_width=self.valves.SVG_WIDTH, + export_format=self.valves.EXPORT_FORMAT, + ) + + await __event_call__( + { + "type": "execute", + "data": {"code": js_code}, + } + ) + + await self._emit_status(__event_emitter__, "✅ 信息图生成完成!", True) + logger.info("Infographic to Markdown completed") + + except Exception as e: + error_message = f"Infographic generation failed: {str(e)}" + logger.error(error_message, exc_info=True) + await self._emit_status(__event_emitter__, f"❌ {error_message}", True) + + return body diff --git a/plugins/actions/js-render-poc/infographic_markdown_cn.py b/plugins/actions/js-render-poc/infographic_markdown_cn.py new file mode 100644 index 0000000..5ee8f90 --- /dev/null +++ b/plugins/actions/js-render-poc/infographic_markdown_cn.py @@ -0,0 +1,592 @@ +""" +title: 📊 信息图转 Markdown +author: Fu-Jie +version: 1.0.0 +description: AI 生成信息图语法,前端渲染 SVG 并转换为 Markdown 图片格式嵌入消息。支持 AntV Infographic 模板。 +""" + +import time +import json +import logging +import re +from typing import Optional, Callable, Awaitable, Any, Dict +from pydantic import BaseModel, Field +from fastapi import Request +from datetime import datetime + +from open_webui.utils.chat import generate_chat_completion +from open_webui.models.users import Users + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# ================================================================= +# LLM 提示词 +# ================================================================= + +SYSTEM_PROMPT_INFOGRAPHIC = """ +你是一位专业的信息图设计专家,能够分析用户提供的文本内容并将其转换为 AntV Infographic 语法格式。 + +## 信息图语法规范 + +信息图语法是一种类似 Mermaid 的声明式语法,用于描述信息图模板、数据和主题。 + +### 语法规则 +- 入口使用 `infographic <模板名>` +- 键值对用空格分隔,**绝对不允许使用冒号** +- 使用两个空格缩进 +- 对象数组使用 `-` 加换行 + +⚠️ **重要警告:这不是 YAML 格式!** +- ❌ 错误:`children:` `items:` `data:`(带冒号) +- ✅ 正确:`children` `items` `data`(不带冒号) + +### 模板库与选择指南 + +根据内容结构选择最合适的模板: + +#### 1. 列表与层级 +- **列表**:`list-grid`(网格卡片)、`list-vertical`(垂直列表) +- **树形**:`tree-vertical`(垂直树)、`tree-horizontal`(水平树) +- **思维导图**:`mindmap`(思维导图) + +#### 2. 序列与关系 +- **流程**:`sequence-roadmap`(路线图)、`sequence-zigzag`(折线流程) +- **关系**:`relation-sankey`(桑基图)、`relation-circle`(圆形关系) + +#### 3. 对比与分析 +- **对比**:`compare-binary`(二元对比) +- **分析**:`compare-swot`(SWOT 分析)、`quadrant-quarter`(象限图) + +#### 4. 图表与数据 +- **图表**:`chart-bar`、`chart-column`、`chart-line`、`chart-pie`、`chart-doughnut`、`chart-area` + +### 数据结构示例 + +#### A. 标准列表/树形 +```infographic +infographic list-grid +data + title 项目模块 + items + - label 模块 A + desc 模块 A 的描述 + - label 模块 B + desc 模块 B 的描述 +``` + +#### B. 二元对比 +```infographic +infographic compare-binary +data + title 优势与劣势 + items + - label 优势 + children + - label 研发能力强 + desc 技术领先 + - label 劣势 + children + - label 品牌曝光弱 + desc 营销不足 +``` + +#### C. 图表 +```infographic +infographic chart-bar +data + title 季度收入 + items + - label Q1 + value 120 + - label Q2 + value 150 +``` + +### 常用数据字段 +- `label`:主标题/标签(必填) +- `desc`:描述文字(`list-grid` 最多 30 个中文字符) +- `value`:数值(用于图表) +- `children`:嵌套项 + +## 输出要求 +1. **语言**:使用用户的语言输出内容。 +2. **格式**:用 ```infographic ... ``` 包裹输出。 +3. **无冒号**:键后面不要使用冒号。 +4. **缩进**:使用 2 个空格。 +""" + +USER_PROMPT_GENERATE = """ +请分析以下文本内容,将其核心信息转换为 AntV Infographic 语法格式。 + +--- +**用户上下文:** +用户名:{user_name} +当前时间:{current_date_time_str} +用户语言:{user_language} +--- + +**文本内容:** +{long_text_content} + +请根据文本特征选择最合适的信息图模板,输出标准的信息图语法。 + +**重要提示:** +- 如果使用 `list-grid` 格式,确保每个卡片的 `desc` 描述限制在 **最多 30 个中文字符**。 +- 描述应简洁,突出重点。 +""" + + +class Action: + class Valves(BaseModel): + SHOW_STATUS: bool = Field( + default=True, description="在聊天界面显示操作状态更新。" + ) + MODEL_ID: str = Field( + default="", + description="用于文本分析的 LLM 模型 ID。留空则使用当前对话模型。", + ) + MIN_TEXT_LENGTH: int = Field( + default=50, + description="信息图分析所需的最小文本长度(字符数)。", + ) + MESSAGE_COUNT: int = Field( + default=1, + description="用于生成的最近消息数量。", + ) + SVG_WIDTH: int = Field( + default=800, + description="生成的 SVG 宽度(像素)。", + ) + EXPORT_FORMAT: str = Field( + default="svg", + description="导出格式:'svg' 或 'png'。", + ) + + def __init__(self): + self.valves = self.Valves() + + def _extract_chat_id(self, body: dict, metadata: Optional[dict]) -> str: + """从 body 或 metadata 中提取 chat_id""" + if isinstance(body, dict): + chat_id = body.get("chat_id") + if isinstance(chat_id, str) and chat_id.strip(): + return chat_id.strip() + + body_metadata = body.get("metadata", {}) + if isinstance(body_metadata, dict): + chat_id = body_metadata.get("chat_id") + if isinstance(chat_id, str) and chat_id.strip(): + return chat_id.strip() + + if isinstance(metadata, dict): + chat_id = metadata.get("chat_id") + if isinstance(chat_id, str) and chat_id.strip(): + return chat_id.strip() + + return "" + + def _extract_message_id(self, body: dict, metadata: Optional[dict]) -> str: + """从 body 或 metadata 中提取 message_id""" + if isinstance(body, dict): + message_id = body.get("id") + if isinstance(message_id, str) and message_id.strip(): + return message_id.strip() + + body_metadata = body.get("metadata", {}) + if isinstance(body_metadata, dict): + message_id = body_metadata.get("message_id") + if isinstance(message_id, str) and message_id.strip(): + return message_id.strip() + + if isinstance(metadata, dict): + message_id = metadata.get("message_id") + if isinstance(message_id, str) and message_id.strip(): + return message_id.strip() + + return "" + + def _extract_infographic_syntax(self, llm_output: str) -> str: + """从 LLM 输出中提取信息图语法""" + match = re.search(r"```infographic\s*(.*?)\s*```", llm_output, re.DOTALL) + if match: + return match.group(1).strip() + else: + logger.warning("LLM 输出未遵循预期格式,将整个输出作为语法处理。") + return llm_output.strip() + + def _extract_text_content(self, content) -> str: + """从消息内容中提取文本,支持多模态格式""" + if isinstance(content, str): + return content + elif isinstance(content, list): + 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 "" + + async def _emit_status(self, emitter, description: str, done: bool = False): + """发送状态更新事件""" + if self.valves.SHOW_STATUS and emitter: + await emitter( + {"type": "status", "data": {"description": description, "done": done}} + ) + + def _generate_js_code( + self, + unique_id: str, + chat_id: str, + message_id: str, + infographic_syntax: str, + svg_width: int, + export_format: str, + ) -> str: + """生成用于前端 SVG 渲染的 JavaScript 代码""" + + # 转义语法以便嵌入 JS + syntax_escaped = ( + infographic_syntax + .replace("\\", "\\\\") + .replace("`", "\\`") + .replace("${", "\\${") + .replace("", "<\\/script>") + ) + + # 模板映射 + template_mapping_js = """ + 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', + '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' + }; + """ + + return f""" +(async function() {{ + const uniqueId = "{unique_id}"; + const chatId = "{chat_id}"; + const messageId = "{message_id}"; + const svgWidth = {svg_width}; + const exportFormat = "{export_format}"; + + console.log("[信息图 Markdown] 开始渲染..."); + console.log("[信息图 Markdown] chatId:", chatId, "messageId:", messageId); + + try {{ + // 加载 AntV Infographic(如果尚未加载) + if (typeof AntVInfographic === 'undefined') {{ + console.log("[信息图 Markdown] 正在加载 AntV Infographic 库..."); + await new Promise((resolve, reject) => {{ + const script = document.createElement('script'); + script.src = 'https://unpkg.com/@antv/infographic@latest/dist/infographic.min.js'; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }}); + console.log("[信息图 Markdown] 库加载完成。"); + }} + + const {{ Infographic }} = AntVInfographic; + + // 获取信息图语法 + let syntaxContent = `{syntax_escaped}`; + console.log("[信息图 Markdown] 原始语法:", syntaxContent.substring(0, 200) + "..."); + + // 清理语法 + const backtick = String.fromCharCode(96); + const prefix = backtick + backtick + backtick + 'infographic'; + const simplePrefix = backtick + backtick + backtick; + + if (syntaxContent.toLowerCase().startsWith(prefix)) {{ + syntaxContent = syntaxContent.substring(prefix.length).trim(); + }} else if (syntaxContent.startsWith(simplePrefix)) {{ + syntaxContent = syntaxContent.substring(simplePrefix.length).trim(); + }} + + if (syntaxContent.endsWith(simplePrefix)) {{ + syntaxContent = syntaxContent.substring(0, syntaxContent.length - simplePrefix.length).trim(); + }} + + // 修复关键字后的冒号 + syntaxContent = syntaxContent.replace(/^(data|items|children|theme|config):/gm, '$1'); + syntaxContent = syntaxContent.replace(/(\\s)(children|items):/g, '$1$2'); + + // 确保有 infographic 前缀 + if (!syntaxContent.trim().toLowerCase().startsWith('infographic')) {{ + syntaxContent = 'infographic list-grid\\n' + syntaxContent; + }} + + // 应用模板映射 + {template_mapping_js} + + for (const [key, value] of Object.entries(TEMPLATE_MAPPING)) {{ + const regex = new RegExp(`infographic\\\\s+${{key}}(?=\\\\s|$)`, 'i'); + if (regex.test(syntaxContent)) {{ + console.log(`[信息图 Markdown] 自动映射: ${{key}} -> ${{value}}`); + syntaxContent = syntaxContent.replace(regex, `infographic ${{value}}`); + break; + }} + }} + + console.log("[信息图 Markdown] 清理后语法:", syntaxContent.substring(0, 200) + "..."); + + // 创建离屏容器 + const container = document.createElement('div'); + container.id = 'infographic-offscreen-' + uniqueId; + container.style.cssText = 'position:absolute;left:-9999px;top:-9999px;width:' + svgWidth + 'px;'; + document.body.appendChild(container); + + // 创建并渲染信息图 + const instance = new Infographic({{ + container: '#' + container.id, + width: svgWidth, + padding: 24, + }}); + + console.log("[信息图 Markdown] 正在渲染信息图..."); + instance.render(syntaxContent); + + // 等待渲染完成并导出 + await new Promise(resolve => setTimeout(resolve, 1000)); + + let dataUrl; + if (exportFormat === 'png') {{ + dataUrl = await instance.toDataURL({{ type: 'png', dpr: 2 }}); + }} else {{ + dataUrl = await instance.toDataURL({{ type: 'svg', embedResources: true }}); + }} + + console.log("[信息图 Markdown] Data URL 已生成,长度:", dataUrl.length); + + // 清理 + instance.destroy(); + document.body.removeChild(container); + + // 生成 Markdown 图片 + const markdownImage = `![📊 AI 生成的信息图](${{dataUrl}})`; + + // 通过 API 更新消息 + if (chatId && messageId) {{ + const token = localStorage.getItem("token"); + + // 获取当前消息内容 + const getResponse = await fetch(`/api/v1/chats/${{chatId}}`, {{ + method: "GET", + headers: {{ "Authorization": `Bearer ${{token}}` }} + }}); + + if (!getResponse.ok) {{ + throw new Error("获取对话数据失败: " + getResponse.status); + }} + + const chatData = await getResponse.json(); + let originalContent = ""; + + if (chatData.chat && chatData.chat.messages) {{ + const targetMsg = chatData.chat.messages.find(m => m.id === messageId); + if (targetMsg && targetMsg.content) {{ + originalContent = targetMsg.content; + }} + }} + + // 移除已有的信息图图片 + const infographicPattern = /\\n*!\\[📊[^\\]]*\\]\\(data:image\\/[^)]+\\)/g; + let cleanedContent = originalContent.replace(infographicPattern, ""); + cleanedContent = cleanedContent.replace(/\\n{{3,}}/g, "\\n\\n").trim(); + + // 追加新图片 + const newContent = cleanedContent + "\\n\\n" + markdownImage; + + // 更新消息 + const updateResponse = await fetch(`/api/v1/chats/${{chatId}}/messages/${{messageId}}/event`, {{ + method: "POST", + headers: {{ + "Content-Type": "application/json", + "Authorization": `Bearer ${{token}}` + }}, + body: JSON.stringify({{ + type: "chat:message", + data: {{ content: newContent }} + }}) + }}); + + if (updateResponse.ok) {{ + console.log("[信息图 Markdown] ✅ 消息更新成功!"); + }} else {{ + console.error("[信息图 Markdown] API 错误:", updateResponse.status); + }} + }} else {{ + console.warn("[信息图 Markdown] ⚠️ 缺少 chatId 或 messageId"); + }} + + }} catch (error) {{ + console.error("[信息图 Markdown] 错误:", error); + }} +}})(); +""" + + async def action( + self, + body: dict, + __user__: dict = None, + __event_emitter__=None, + __event_call__: Optional[Callable[[Any], Awaitable[None]]] = None, + __metadata__: Optional[dict] = None, + __request__: Request = None, + ) -> dict: + """ + 使用 AntV 生成信息图并作为 Markdown 图片嵌入。 + """ + logger.info("动作:信息图转 Markdown 开始") + + # 获取用户信息 + if isinstance(__user__, (list, tuple)): + user_language = __user__[0].get("language", "zh") if __user__ else "zh" + user_name = __user__[0].get("name", "用户") if __user__[0] else "用户" + user_id = __user__[0].get("id", "unknown_user") if __user__ else "unknown_user" + elif isinstance(__user__, dict): + user_language = __user__.get("language", "zh") + user_name = __user__.get("name", "用户") + user_id = __user__.get("id", "unknown_user") + else: + user_language = "zh" + user_name = "用户" + user_id = "unknown_user" + + # 获取当前时间 + now = datetime.now() + current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S") + + try: + messages = body.get("messages", []) + if not messages: + raise ValueError("没有可用的消息。") + + # 获取最近的消息 + message_count = min(self.valves.MESSAGE_COUNT, len(messages)) + recent_messages = messages[-message_count:] + + # 聚合内容 + aggregated_parts = [] + for msg in recent_messages: + text_content = self._extract_text_content(msg.get("content")) + if text_content: + aggregated_parts.append(text_content) + + if not aggregated_parts: + raise ValueError("消息中未找到文本内容。") + + long_text_content = "\n\n---\n\n".join(aggregated_parts) + + # 移除已有的 HTML 块 + parts = re.split(r"```html.*?```", long_text_content, flags=re.DOTALL) + clean_content = "" + for part in reversed(parts): + if part.strip(): + clean_content = part.strip() + break + + if not clean_content: + clean_content = long_text_content.strip() + + # 检查最小长度 + if len(clean_content) < self.valves.MIN_TEXT_LENGTH: + await self._emit_status( + __event_emitter__, + f"⚠️ 内容太短({len(clean_content)} 字符),至少需要 {self.valves.MIN_TEXT_LENGTH} 字符", + True, + ) + return body + + await self._emit_status(__event_emitter__, "📊 正在分析内容...", False) + + # 通过 LLM 生成信息图语法 + formatted_user_prompt = USER_PROMPT_GENERATE.format( + user_name=user_name, + current_date_time_str=current_date_time_str, + user_language=user_language, + long_text_content=clean_content, + ) + + target_model = self.valves.MODEL_ID or body.get("model") + + llm_payload = { + "model": target_model, + "messages": [ + {"role": "system", "content": SYSTEM_PROMPT_INFOGRAPHIC}, + {"role": "user", "content": formatted_user_prompt}, + ], + "stream": False, + } + + user_obj = Users.get_user_by_id(user_id) + if not user_obj: + raise ValueError(f"无法获取用户对象:{user_id}") + + await self._emit_status(__event_emitter__, "📊 AI 正在生成信息图语法...", False) + + llm_response = await generate_chat_completion(__request__, llm_payload, user_obj) + + if not llm_response or "choices" not in llm_response or not llm_response["choices"]: + raise ValueError("无效的 LLM 响应。") + + assistant_content = llm_response["choices"][0]["message"]["content"] + infographic_syntax = self._extract_infographic_syntax(assistant_content) + + logger.info(f"生成的语法:{infographic_syntax[:200]}...") + + # 提取 API 回调所需的 ID + chat_id = self._extract_chat_id(body, __metadata__) + message_id = self._extract_message_id(body, __metadata__) + unique_id = f"ig_{int(time.time() * 1000)}" + + await self._emit_status(__event_emitter__, "📊 正在渲染 SVG...", False) + + # 执行 JS 进行渲染和嵌入 + if __event_call__: + js_code = self._generate_js_code( + unique_id=unique_id, + chat_id=chat_id, + message_id=message_id, + infographic_syntax=infographic_syntax, + svg_width=self.valves.SVG_WIDTH, + export_format=self.valves.EXPORT_FORMAT, + ) + + await __event_call__( + { + "type": "execute", + "data": {"code": js_code}, + } + ) + + await self._emit_status(__event_emitter__, "✅ 信息图生成完成!", True) + logger.info("信息图转 Markdown 完成") + + except Exception as e: + error_message = f"信息图生成失败:{str(e)}" + logger.error(error_message, exc_info=True) + await self._emit_status(__event_emitter__, f"❌ {error_message}", True) + + return body