Compare commits
11 Commits
v2026.01.0
...
v2026.01.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cc4478dd9 | ||
|
|
59f6f2ba97 | ||
|
|
172d9e0b41 | ||
|
|
de7086c9e1 | ||
|
|
5f63e8d1e2 | ||
|
|
3da0b894fd | ||
|
|
ad2d26aa16 | ||
|
|
a09f3e0bdb | ||
|
|
3a0faf27df | ||
|
|
cd3e7309a8 | ||
|
|
54cc10bb41 |
68
.github/workflows/publish_new_plugin.yml
vendored
Normal file
68
.github/workflows/publish_new_plugin.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
name: Publish New Plugin
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
plugin_dir:
|
||||||
|
description: 'Plugin directory (e.g., plugins/actions/deep-dive)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
dry_run:
|
||||||
|
description: 'Dry run mode (preview only)'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install requests
|
||||||
|
|
||||||
|
- name: Validate plugin directory
|
||||||
|
run: |
|
||||||
|
if [ ! -d "${{ github.event.inputs.plugin_dir }}" ]; then
|
||||||
|
echo "❌ Error: Directory '${{ github.event.inputs.plugin_dir }}' does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ Found plugin directory: ${{ github.event.inputs.plugin_dir }}"
|
||||||
|
ls -la "${{ github.event.inputs.plugin_dir }}"
|
||||||
|
|
||||||
|
- name: Publish Plugin
|
||||||
|
env:
|
||||||
|
OPENWEBUI_API_KEY: ${{ secrets.OPENWEBUI_API_KEY }}
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event.inputs.dry_run }}" = "true" ]; then
|
||||||
|
echo "🔍 Dry run mode - previewing..."
|
||||||
|
python scripts/publish_plugin.py --new "${{ github.event.inputs.plugin_dir }}" --dry-run
|
||||||
|
else
|
||||||
|
echo "🚀 Publishing plugin..."
|
||||||
|
python scripts/publish_plugin.py --new "${{ github.event.inputs.plugin_dir }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit changes (if ID was added)
|
||||||
|
if: ${{ github.event.inputs.dry_run != 'true' }}
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
# Check if there are changes to commit
|
||||||
|
if git diff --quiet; then
|
||||||
|
echo "No changes to commit"
|
||||||
|
else
|
||||||
|
git add "${{ github.event.inputs.plugin_dir }}"
|
||||||
|
git commit -m "feat: add openwebui_id to ${{ github.event.inputs.plugin_dir }}"
|
||||||
|
git push
|
||||||
|
echo "✅ Committed and pushed openwebui_id changes"
|
||||||
|
fi
|
||||||
18
README.md
18
README.md
@@ -7,26 +7,26 @@ A collection of enhancements, plugins, and prompts for [OpenWebUI](https://githu
|
|||||||
<!-- STATS_START -->
|
<!-- STATS_START -->
|
||||||
## 📊 Community Stats
|
## 📊 Community Stats
|
||||||
|
|
||||||
> 🕐 Auto-updated: 2026-01-08 00:11
|
> 🕐 Auto-updated: 2026-01-08 08:35
|
||||||
|
|
||||||
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|
| 👤 Author | 👥 Followers | ⭐ Points | 🏆 Contributions |
|
||||||
|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|
|
||||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **49** | **63** | **18** |
|
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **50** | **64** | **18** |
|
||||||
|
|
||||||
| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |
|
| 📝 Posts | ⬇️ Downloads | 👁️ Views | 👍 Upvotes | 💾 Saves |
|
||||||
|:---:|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|:---:|
|
||||||
| **11** | **889** | **9358** | **55** | **48** |
|
| **11** | **916** | **9670** | **55** | **50** |
|
||||||
|
|
||||||
### 🔥 Top 6 Popular Plugins
|
### 🔥 Top 6 Popular Plugins
|
||||||
|
|
||||||
| Rank | Plugin | Downloads | Views |
|
| Rank | Plugin | Downloads | Views |
|
||||||
|:---:|------|:---:|:---:|
|
|:---:|------|:---:|:---:|
|
||||||
| 🥇 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 283 | 2441 |
|
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 294 | 2550 |
|
||||||
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 175 | 486 |
|
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 178 | 507 |
|
||||||
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 118 | 1287 |
|
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 119 | 1308 |
|
||||||
| 4️⃣ | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | 82 | 1528 |
|
| 4️⃣ | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 87 | 1123 |
|
||||||
| 5️⃣ | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 80 | 1081 |
|
| 5️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 84 | 1561 |
|
||||||
| 6️⃣ | [Export to Word (Enhanced Formatting)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 67 | 605 |
|
| 6️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 69 | 644 |
|
||||||
|
|
||||||
*See full stats in [Community Stats Report](./docs/community-stats.md)*
|
*See full stats in [Community Stats Report](./docs/community-stats.md)*
|
||||||
<!-- STATS_END -->
|
<!-- STATS_END -->
|
||||||
|
|||||||
18
README_CN.md
18
README_CN.md
@@ -7,26 +7,26 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词
|
|||||||
<!-- STATS_START -->
|
<!-- STATS_START -->
|
||||||
## 📊 社区统计
|
## 📊 社区统计
|
||||||
|
|
||||||
> 🕐 自动更新于 2026-01-08 00:11
|
> 🕐 自动更新于 2026-01-08 08:35
|
||||||
|
|
||||||
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|
| 👤 作者 | 👥 粉丝 | ⭐ 积分 | 🏆 贡献 |
|
||||||
|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|
|
||||||
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **49** | **63** | **18** |
|
| [Fu-Jie](https://openwebui.com/u/Fu-Jie) | **50** | **64** | **18** |
|
||||||
|
|
||||||
| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |
|
| 📝 发布 | ⬇️ 下载 | 👁️ 浏览 | 👍 点赞 | 💾 收藏 |
|
||||||
|:---:|:---:|:---:|:---:|:---:|
|
|:---:|:---:|:---:|:---:|:---:|
|
||||||
| **11** | **889** | **9358** | **55** | **48** |
|
| **11** | **916** | **9670** | **55** | **50** |
|
||||||
|
|
||||||
### 🔥 热门插件 Top 6
|
### 🔥 热门插件 Top 6
|
||||||
|
|
||||||
| 排名 | 插件 | 下载 | 浏览 |
|
| 排名 | 插件 | 下载 | 浏览 |
|
||||||
|:---:|------|:---:|:---:|
|
|:---:|------|:---:|:---:|
|
||||||
| 🥇 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 283 | 2441 |
|
| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | 294 | 2550 |
|
||||||
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 175 | 486 |
|
| 🥈 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | 178 | 507 |
|
||||||
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 118 | 1287 |
|
| 🥉 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | 119 | 1308 |
|
||||||
| 4️⃣ | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | 82 | 1528 |
|
| 4️⃣ | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 87 | 1123 |
|
||||||
| 5️⃣ | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | 80 | 1081 |
|
| 5️⃣ | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | 84 | 1561 |
|
||||||
| 6️⃣ | [Export to Word (Enhanced Formatting)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 67 | 605 |
|
| 6️⃣ | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | 69 | 644 |
|
||||||
|
|
||||||
*完整统计请查看 [社区统计报告](./docs/community-stats.zh.md)*
|
*完整统计请查看 [社区统计报告](./docs/community-stats.zh.md)*
|
||||||
<!-- STATS_END -->
|
<!-- STATS_END -->
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"total_posts": 11,
|
"total_posts": 11,
|
||||||
"total_downloads": 889,
|
"total_downloads": 916,
|
||||||
"total_views": 9358,
|
"total_views": 9670,
|
||||||
"total_upvotes": 55,
|
"total_upvotes": 55,
|
||||||
"total_downvotes": 1,
|
"total_downvotes": 1,
|
||||||
"total_saves": 48,
|
"total_saves": 50,
|
||||||
"total_comments": 15,
|
"total_comments": 15,
|
||||||
"by_type": {
|
"by_type": {
|
||||||
"action": 9,
|
"action": 9,
|
||||||
@@ -12,35 +12,35 @@
|
|||||||
},
|
},
|
||||||
"posts": [
|
"posts": [
|
||||||
{
|
{
|
||||||
"title": "Turn Any Text into Beautiful Mind Maps",
|
"title": "Smart Mind Map",
|
||||||
"slug": "turn_any_text_into_beautiful_mind_maps_3094c59a",
|
"slug": "turn_any_text_into_beautiful_mind_maps_3094c59a",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "0.9.1",
|
"version": "0.9.1",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.",
|
"description": "Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge.",
|
||||||
"downloads": 283,
|
"downloads": 294,
|
||||||
"views": 2441,
|
"views": 2550,
|
||||||
"upvotes": 10,
|
"upvotes": 10,
|
||||||
"saves": 15,
|
"saves": 16,
|
||||||
"comments": 10,
|
"comments": 10,
|
||||||
"created_at": "2025-12-30",
|
"created_at": "2025-12-30",
|
||||||
"updated_at": "2026-01-06",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a"
|
"url": "https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Export to Excel",
|
"title": "Export to Excel",
|
||||||
"slug": "export_mulit_table_to_excel_244b8f9d",
|
"slug": "export_mulit_table_to_excel_244b8f9d",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "0.3.6",
|
"version": "0.3.7",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting.",
|
"description": "Extracts tables from chat messages and exports them to Excel (.xlsx) files with smart formatting.",
|
||||||
"downloads": 175,
|
"downloads": 178,
|
||||||
"views": 486,
|
"views": 507,
|
||||||
"upvotes": 3,
|
"upvotes": 3,
|
||||||
"saves": 3,
|
"saves": 3,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2025-05-30",
|
"created_at": "2025-05-30",
|
||||||
"updated_at": "2026-01-03",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d"
|
"url": "https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -49,41 +49,25 @@
|
|||||||
"type": "filter",
|
"type": "filter",
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "This filter automatically compresses long conversation contexts by intelligently summarizing and removing intermediate messages while preserving critical information, thereby significantly reducing token consumption.",
|
"description": "Reduces token consumption in long conversations while maintaining coherence through intelligent summarization and message compression.",
|
||||||
"downloads": 118,
|
"downloads": 119,
|
||||||
"views": 1287,
|
"views": 1308,
|
||||||
"upvotes": 5,
|
"upvotes": 5,
|
||||||
"saves": 9,
|
"saves": 9,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2025-11-08",
|
"created_at": "2025-11-08",
|
||||||
"updated_at": "2025-12-31",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/async_context_compression_b1655bc8"
|
"url": "https://openwebui.com/posts/async_context_compression_b1655bc8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Flash Card ",
|
"title": "📊 Smart Infographic (AntV)",
|
||||||
"slug": "flash_card_65a2ea8f",
|
|
||||||
"type": "action",
|
|
||||||
"version": "0.2.4",
|
|
||||||
"author": "Fu-Jie",
|
|
||||||
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
|
|
||||||
"downloads": 82,
|
|
||||||
"views": 1528,
|
|
||||||
"upvotes": 8,
|
|
||||||
"saves": 5,
|
|
||||||
"comments": 2,
|
|
||||||
"created_at": "2025-12-30",
|
|
||||||
"updated_at": "2026-01-03",
|
|
||||||
"url": "https://openwebui.com/posts/flash_card_65a2ea8f"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Smart Infographic",
|
|
||||||
"slug": "smart_infographic_ad6f0c7f",
|
"slug": "smart_infographic_ad6f0c7f",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "1.4.0",
|
"version": "1.4.1",
|
||||||
"author": "jeff",
|
"author": "jeff",
|
||||||
"description": "AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.",
|
"description": "AI-powered infographic generator based on AntV Infographic. Supports professional templates, auto-icon matching, and SVG/PNG downloads.",
|
||||||
"downloads": 80,
|
"downloads": 87,
|
||||||
"views": 1081,
|
"views": 1123,
|
||||||
"upvotes": 7,
|
"upvotes": 7,
|
||||||
"saves": 8,
|
"saves": 8,
|
||||||
"comments": 2,
|
"comments": 2,
|
||||||
@@ -92,83 +76,99 @@
|
|||||||
"url": "https://openwebui.com/posts/smart_infographic_ad6f0c7f"
|
"url": "https://openwebui.com/posts/smart_infographic_ad6f0c7f"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Export to Word (Enhanced Formatting)",
|
"title": "Flash Card",
|
||||||
|
"slug": "flash_card_65a2ea8f",
|
||||||
|
"type": "action",
|
||||||
|
"version": "0.2.4",
|
||||||
|
"author": "Fu-Jie",
|
||||||
|
"description": "Quickly generates beautiful flashcards from text, extracting key points and categories.",
|
||||||
|
"downloads": 84,
|
||||||
|
"views": 1561,
|
||||||
|
"upvotes": 8,
|
||||||
|
"saves": 5,
|
||||||
|
"comments": 2,
|
||||||
|
"created_at": "2025-12-30",
|
||||||
|
"updated_at": "2026-01-07",
|
||||||
|
"url": "https://openwebui.com/posts/flash_card_65a2ea8f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Export to Word (Enhanced)",
|
||||||
"slug": "export_to_word_enhanced_formatting_fca6a315",
|
"slug": "export_to_word_enhanced_formatting_fca6a315",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "0.4.2",
|
"version": "0.4.3",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "Export the current conversation to a formatted Word doc with syntax highlighting, AI-generated titles, and perfect Markdown rendering (tables, quotes, lists).",
|
"description": "Export current conversation from Markdown to Word (.docx) with Mermaid diagrams rendered client-side (Mermaid.js, SVG+PNG), LaTeX math, real hyperlinks, improved tables, syntax highlighting, and blockquote support.",
|
||||||
"downloads": 67,
|
"downloads": 69,
|
||||||
"views": 605,
|
"views": 644,
|
||||||
"upvotes": 5,
|
"upvotes": 5,
|
||||||
"saves": 4,
|
"saves": 5,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2026-01-03",
|
"created_at": "2026-01-03",
|
||||||
"updated_at": "2026-01-07",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315"
|
"url": "https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "智能信息图",
|
"title": "📊 智能信息图 (AntV Infographic)",
|
||||||
"slug": "智能信息图_e04a48ff",
|
"slug": "智能信息图_e04a48ff",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "1.3.1",
|
"version": "1.4.1",
|
||||||
"author": "jeff",
|
"author": "jeff",
|
||||||
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
|
"description": "基于 AntV Infographic 的智能信息图生成插件。支持多种专业模板,自动图标匹配,并提供 SVG/PNG 下载功能。",
|
||||||
"downloads": 33,
|
"downloads": 33,
|
||||||
"views": 426,
|
"views": 434,
|
||||||
"upvotes": 3,
|
"upvotes": 3,
|
||||||
"saves": 0,
|
"saves": 0,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2025-12-28",
|
"created_at": "2025-12-28",
|
||||||
"updated_at": "2025-12-29",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/智能信息图_e04a48ff"
|
"url": "https://openwebui.com/posts/智能信息图_e04a48ff"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "导出为 Word-支持公式、流程图、表格和代码块",
|
"title": "导出为 Word (增强版)",
|
||||||
"slug": "导出为_word_支持公式流程图表格和代码块_8a6306c0",
|
"slug": "导出为_word_支持公式流程图表格和代码块_8a6306c0",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "0.4.1",
|
"version": "0.4.3",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "将当前对话内容从 Markdown 转换并导出为 Word (.docx) 文件,支持中英文无乱码。",
|
"description": "将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。",
|
||||||
"downloads": 20,
|
"downloads": 20,
|
||||||
"views": 799,
|
"views": 815,
|
||||||
"upvotes": 7,
|
"upvotes": 7,
|
||||||
"saves": 1,
|
"saves": 1,
|
||||||
"comments": 1,
|
"comments": 1,
|
||||||
"created_at": "2026-01-04",
|
"created_at": "2026-01-04",
|
||||||
"updated_at": "2026-01-05",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0"
|
"url": "https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "智能生成交互式思维导图,帮助用户可视化知识",
|
"title": "思维导图",
|
||||||
"slug": "智能生成交互式思维导图帮助用户可视化知识_8d4b097b",
|
"slug": "智能生成交互式思维导图帮助用户可视化知识_8d4b097b",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "0.8.0",
|
"version": "0.9.1",
|
||||||
"author": "",
|
"author": "Fu-Jie",
|
||||||
"description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。",
|
"description": "智能分析文本内容,生成交互式思维导图,帮助用户结构化和可视化知识。",
|
||||||
"downloads": 14,
|
"downloads": 15,
|
||||||
"views": 263,
|
"views": 273,
|
||||||
"upvotes": 2,
|
"upvotes": 2,
|
||||||
"saves": 1,
|
"saves": 1,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2025-12-31",
|
"created_at": "2025-12-31",
|
||||||
"updated_at": "2025-12-31",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b"
|
"url": "https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "闪记卡生成插件",
|
"title": "闪记卡 (Flash Card)",
|
||||||
"slug": "闪记卡生成插件_4a31eac3",
|
"slug": "闪记卡生成插件_4a31eac3",
|
||||||
"type": "action",
|
"type": "action",
|
||||||
"version": "0.2.2",
|
"version": "0.2.4",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。",
|
"description": "快速将文本提炼为精美的学习记忆卡片,支持核心要点提取与分类。",
|
||||||
"downloads": 12,
|
"downloads": 12,
|
||||||
"views": 320,
|
"views": 329,
|
||||||
"upvotes": 3,
|
"upvotes": 3,
|
||||||
"saves": 1,
|
"saves": 1,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2025-12-30",
|
"created_at": "2025-12-30",
|
||||||
"updated_at": "2025-12-31",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/闪记卡生成插件_4a31eac3"
|
"url": "https://openwebui.com/posts/闪记卡生成插件_4a31eac3"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -177,14 +177,14 @@
|
|||||||
"type": "filter",
|
"type": "filter",
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"author": "Fu-Jie",
|
"author": "Fu-Jie",
|
||||||
"description": "在 LLM 响应完成后进行上下文摘要和压缩",
|
"description": "通过智能摘要和消息压缩,降低长对话的 token 消耗,同时保持对话连贯性。",
|
||||||
"downloads": 5,
|
"downloads": 5,
|
||||||
"views": 122,
|
"views": 126,
|
||||||
"upvotes": 2,
|
"upvotes": 2,
|
||||||
"saves": 1,
|
"saves": 1,
|
||||||
"comments": 0,
|
"comments": 0,
|
||||||
"created_at": "2025-11-08",
|
"created_at": "2025-11-08",
|
||||||
"updated_at": "2025-12-31",
|
"updated_at": "2026-01-07",
|
||||||
"url": "https://openwebui.com/posts/异步上下文压缩_5c0617cb"
|
"url": "https://openwebui.com/posts/异步上下文压缩_5c0617cb"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -193,11 +193,11 @@
|
|||||||
"name": "Fu-Jie",
|
"name": "Fu-Jie",
|
||||||
"profile_url": "https://openwebui.com/u/Fu-Jie",
|
"profile_url": "https://openwebui.com/u/Fu-Jie",
|
||||||
"profile_image": "https://community.s3.openwebui.com/uploads/users/b15d1348-4347-42b4-b815-e053342d6cb0/profile_d9510745-4bd4-4f8f-a997-4a21847d9300.webp",
|
"profile_image": "https://community.s3.openwebui.com/uploads/users/b15d1348-4347-42b4-b815-e053342d6cb0/profile_d9510745-4bd4-4f8f-a997-4a21847d9300.webp",
|
||||||
"followers": 49,
|
"followers": 50,
|
||||||
"following": 2,
|
"following": 2,
|
||||||
"total_points": 63,
|
"total_points": 64,
|
||||||
"post_points": 54,
|
"post_points": 54,
|
||||||
"comment_points": 9,
|
"comment_points": 10,
|
||||||
"contributions": 18
|
"contributions": 18
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
# 📊 OpenWebUI Community Stats Report
|
# 📊 OpenWebUI Community Stats Report
|
||||||
|
|
||||||
> 📅 Updated: 2026-01-08 00:11
|
> 📅 Updated: 2026-01-08 08:35
|
||||||
|
|
||||||
## 📈 Overview
|
## 📈 Overview
|
||||||
|
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 📝 Total Posts | 11 |
|
| 📝 Total Posts | 11 |
|
||||||
| ⬇️ Total Downloads | 889 |
|
| ⬇️ Total Downloads | 916 |
|
||||||
| 👁️ Total Views | 9358 |
|
| 👁️ Total Views | 9670 |
|
||||||
| 👍 Total Upvotes | 55 |
|
| 👍 Total Upvotes | 55 |
|
||||||
| 💾 Total Saves | 48 |
|
| 💾 Total Saves | 50 |
|
||||||
| 💬 Total Comments | 15 |
|
| 💬 Total Comments | 15 |
|
||||||
|
|
||||||
## 📂 By Type
|
## 📂 By Type
|
||||||
@@ -22,14 +22,14 @@
|
|||||||
|
|
||||||
| Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |
|
| Rank | Title | Type | Version | Downloads | Views | Upvotes | Saves | Updated |
|
||||||
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| 1 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 283 | 2441 | 10 | 15 | 2026-01-06 |
|
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 294 | 2550 | 10 | 16 | 2026-01-07 |
|
||||||
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.6 | 175 | 486 | 3 | 3 | 2026-01-03 |
|
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 178 | 507 | 3 | 3 | 2026-01-07 |
|
||||||
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 118 | 1287 | 5 | 9 | 2025-12-31 |
|
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 119 | 1308 | 5 | 9 | 2026-01-07 |
|
||||||
| 4 | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 82 | 1528 | 8 | 5 | 2026-01-03 |
|
| 4 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.1 | 87 | 1123 | 7 | 8 | 2026-01-07 |
|
||||||
| 5 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.0 | 80 | 1081 | 7 | 8 | 2026-01-07 |
|
| 5 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 84 | 1561 | 8 | 5 | 2026-01-07 |
|
||||||
| 6 | [Export to Word (Enhanced Formatting)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.2 | 67 | 605 | 5 | 4 | 2026-01-07 |
|
| 6 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 69 | 644 | 5 | 5 | 2026-01-07 |
|
||||||
| 7 | [智能信息图](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.3.1 | 33 | 426 | 3 | 0 | 2025-12-29 |
|
| 7 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.1 | 33 | 434 | 3 | 0 | 2026-01-07 |
|
||||||
| 8 | [导出为 Word-支持公式、流程图、表格和代码块](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.1 | 20 | 799 | 7 | 1 | 2026-01-05 |
|
| 8 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 20 | 815 | 7 | 1 | 2026-01-07 |
|
||||||
| 9 | [智能生成交互式思维导图,帮助用户可视化知识](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.8.0 | 14 | 263 | 2 | 1 | 2025-12-31 |
|
| 9 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 15 | 273 | 2 | 1 | 2026-01-07 |
|
||||||
| 10 | [闪记卡生成插件](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.2 | 12 | 320 | 3 | 1 | 2025-12-31 |
|
| 10 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 12 | 329 | 3 | 1 | 2026-01-07 |
|
||||||
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 5 | 122 | 2 | 1 | 2025-12-31 |
|
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 5 | 126 | 2 | 1 | 2026-01-07 |
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
# 📊 OpenWebUI 社区统计报告
|
# 📊 OpenWebUI 社区统计报告
|
||||||
|
|
||||||
> 📅 更新时间: 2026-01-08 00:11
|
> 📅 更新时间: 2026-01-08 08:35
|
||||||
|
|
||||||
## 📈 总览
|
## 📈 总览
|
||||||
|
|
||||||
| 指标 | 数值 |
|
| 指标 | 数值 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 📝 发布数量 | 11 |
|
| 📝 发布数量 | 11 |
|
||||||
| ⬇️ 总下载量 | 889 |
|
| ⬇️ 总下载量 | 916 |
|
||||||
| 👁️ 总浏览量 | 9358 |
|
| 👁️ 总浏览量 | 9670 |
|
||||||
| 👍 总点赞数 | 55 |
|
| 👍 总点赞数 | 55 |
|
||||||
| 💾 总收藏数 | 48 |
|
| 💾 总收藏数 | 50 |
|
||||||
| 💬 总评论数 | 15 |
|
| 💬 总评论数 | 15 |
|
||||||
|
|
||||||
## 📂 按类型分类
|
## 📂 按类型分类
|
||||||
@@ -22,14 +22,14 @@
|
|||||||
|
|
||||||
| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |
|
| 排名 | 标题 | 类型 | 版本 | 下载 | 浏览 | 点赞 | 收藏 | 更新日期 |
|
||||||
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|:---:|------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
| 1 | [Turn Any Text into Beautiful Mind Maps](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 283 | 2441 | 10 | 15 | 2026-01-06 |
|
| 1 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | action | 0.9.1 | 294 | 2550 | 10 | 16 | 2026-01-07 |
|
||||||
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.6 | 175 | 486 | 3 | 3 | 2026-01-03 |
|
| 2 | [Export to Excel](https://openwebui.com/posts/export_mulit_table_to_excel_244b8f9d) | action | 0.3.7 | 178 | 507 | 3 | 3 | 2026-01-07 |
|
||||||
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 118 | 1287 | 5 | 9 | 2025-12-31 |
|
| 3 | [Async Context Compression](https://openwebui.com/posts/async_context_compression_b1655bc8) | filter | 1.1.0 | 119 | 1308 | 5 | 9 | 2026-01-07 |
|
||||||
| 4 | [Flash Card ](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 82 | 1528 | 8 | 5 | 2026-01-03 |
|
| 4 | [📊 Smart Infographic (AntV)](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.1 | 87 | 1123 | 7 | 8 | 2026-01-07 |
|
||||||
| 5 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | action | 1.4.0 | 80 | 1081 | 7 | 8 | 2026-01-07 |
|
| 5 | [Flash Card](https://openwebui.com/posts/flash_card_65a2ea8f) | action | 0.2.4 | 84 | 1561 | 8 | 5 | 2026-01-07 |
|
||||||
| 6 | [Export to Word (Enhanced Formatting)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.2 | 67 | 605 | 5 | 4 | 2026-01-07 |
|
| 6 | [Export to Word (Enhanced)](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | action | 0.4.3 | 69 | 644 | 5 | 5 | 2026-01-07 |
|
||||||
| 7 | [智能信息图](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.3.1 | 33 | 426 | 3 | 0 | 2025-12-29 |
|
| 7 | [📊 智能信息图 (AntV Infographic)](https://openwebui.com/posts/智能信息图_e04a48ff) | action | 1.4.1 | 33 | 434 | 3 | 0 | 2026-01-07 |
|
||||||
| 8 | [导出为 Word-支持公式、流程图、表格和代码块](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.1 | 20 | 799 | 7 | 1 | 2026-01-05 |
|
| 8 | [导出为 Word (增强版)](https://openwebui.com/posts/导出为_word_支持公式流程图表格和代码块_8a6306c0) | action | 0.4.3 | 20 | 815 | 7 | 1 | 2026-01-07 |
|
||||||
| 9 | [智能生成交互式思维导图,帮助用户可视化知识](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.8.0 | 14 | 263 | 2 | 1 | 2025-12-31 |
|
| 9 | [思维导图](https://openwebui.com/posts/智能生成交互式思维导图帮助用户可视化知识_8d4b097b) | action | 0.9.1 | 15 | 273 | 2 | 1 | 2026-01-07 |
|
||||||
| 10 | [闪记卡生成插件](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.2 | 12 | 320 | 3 | 1 | 2025-12-31 |
|
| 10 | [闪记卡 (Flash Card)](https://openwebui.com/posts/闪记卡生成插件_4a31eac3) | action | 0.2.4 | 12 | 329 | 3 | 1 | 2026-01-07 |
|
||||||
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 5 | 122 | 2 | 1 | 2025-12-31 |
|
| 11 | [异步上下文压缩](https://openwebui.com/posts/异步上下文压缩_5c0617cb) | filter | 1.1.0 | 5 | 126 | 2 | 1 | 2026-01-07 |
|
||||||
|
|||||||
111
docs/plugins/actions/deep-dive.md
Normal file
111
docs/plugins/actions/deep-dive.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# Deep Dive
|
||||||
|
|
||||||
|
<span class="category-badge action">Action</span>
|
||||||
|
<span class="version-badge">v1.0.0</span>
|
||||||
|
|
||||||
|
A comprehensive thinking lens that dives deep into any content - from context to logic, insights, and action paths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Deep Dive plugin transforms how you understand complex content by guiding you through a structured thinking process. Rather than just summarizing, it deconstructs content across four phases:
|
||||||
|
|
||||||
|
- **🔍 The Context (What?)**: Panoramic view of the situation and background
|
||||||
|
- **🧠 The Logic (Why?)**: Deconstruction of reasoning and mental models
|
||||||
|
- **💎 The Insight (So What?)**: Non-obvious value and hidden implications
|
||||||
|
- **🚀 The Path (Now What?)**: Specific, prioritized strategic actions
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- :material-brain: **Thinking Chain**: Complete structured analysis process
|
||||||
|
- :material-eye: **Deep Understanding**: Reveals hidden assumptions and blind spots
|
||||||
|
- :material-lightbulb-on: **Insight Extraction**: Finds the "Aha!" moments
|
||||||
|
- :material-rocket-launch: **Action Oriented**: Translates understanding into actionable steps
|
||||||
|
- :material-theme-light-dark: **Theme Adaptive**: Auto-adapts to OpenWebUI light/dark theme
|
||||||
|
- :material-translate: **Multi-language**: Outputs in user's preferred language
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Download the plugin file: [`deep_dive.py`](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/deep-dive)
|
||||||
|
2. Upload to OpenWebUI: **Admin Panel** → **Settings** → **Functions**
|
||||||
|
3. Enable the plugin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Provide any long text, article, or meeting notes in the chat
|
||||||
|
2. Click the **Deep Dive** button in the message action bar
|
||||||
|
3. Follow the visual timeline from Context → Logic → Insight → Path
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `SHOW_STATUS` | boolean | `true` | Show status updates during processing |
|
||||||
|
| `MODEL_ID` | string | `""` | LLM model for analysis (empty = current model) |
|
||||||
|
| `MIN_TEXT_LENGTH` | integer | `200` | Minimum text length for analysis |
|
||||||
|
| `CLEAR_PREVIOUS_HTML` | boolean | `true` | Clear previous plugin results |
|
||||||
|
| `MESSAGE_COUNT` | integer | `1` | Number of recent messages to analyze |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Theme Support
|
||||||
|
|
||||||
|
Deep Dive automatically adapts to OpenWebUI's light/dark theme:
|
||||||
|
|
||||||
|
- Detects theme from parent document `<meta name="theme-color">` tag
|
||||||
|
- Falls back to `html/body` class or `data-theme` attribute
|
||||||
|
- Uses system preference `prefers-color-scheme: dark` as last resort
|
||||||
|
|
||||||
|
!!! tip "For Best Results"
|
||||||
|
Enable **iframe Sandbox Allow Same Origin** in OpenWebUI:
|
||||||
|
**Settings** → **Interface** → **Artifacts** → Check **iframe Sandbox Allow Same Origin**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
The plugin generates a beautiful structured timeline:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 🌊 Deep Dive Analysis │
|
||||||
|
│ 👤 User 📅 Date 📊 Word count │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ 🔍 Phase 01: The Context │
|
||||||
|
│ [High-level panoramic view] │
|
||||||
|
│ │
|
||||||
|
│ 🧠 Phase 02: The Logic │
|
||||||
|
│ • Reasoning structure... │
|
||||||
|
│ • Hidden assumptions... │
|
||||||
|
│ │
|
||||||
|
│ 💎 Phase 03: The Insight │
|
||||||
|
│ • Non-obvious value... │
|
||||||
|
│ • Blind spots revealed... │
|
||||||
|
│ │
|
||||||
|
│ 🚀 Phase 04: The Path │
|
||||||
|
│ ▸ Priority Action 1... │
|
||||||
|
│ ▸ Priority Action 2... │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
!!! note "Prerequisites"
|
||||||
|
- OpenWebUI v0.3.0 or later
|
||||||
|
- Uses the active LLM model for analysis
|
||||||
|
- Requires `markdown` Python package
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source Code
|
||||||
|
|
||||||
|
[:fontawesome-brands-github: View on GitHub](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/deep-dive){ .md-button }
|
||||||
111
docs/plugins/actions/deep-dive.zh.md
Normal file
111
docs/plugins/actions/deep-dive.zh.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# 精读 (Deep Dive)
|
||||||
|
|
||||||
|
<span class="category-badge action">Action</span>
|
||||||
|
<span class="version-badge">v1.0.0</span>
|
||||||
|
|
||||||
|
全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
精读插件改变了您理解复杂内容的方式,通过结构化的思维过程引导您进行深度分析。它不仅仅是摘要,而是从四个阶段解构内容:
|
||||||
|
|
||||||
|
- **🔍 全景 (The Context)**: 情境与背景的高层级全景视图
|
||||||
|
- **🧠 脉络 (The Logic)**: 解构底层推理逻辑与思维模型
|
||||||
|
- **💎 洞察 (The Insight)**: 提取非显性价值与隐藏含义
|
||||||
|
- **🚀 路径 (The Path)**: 具体的、按优先级排列的战略行动
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- :material-brain: **思维链**: 完整的结构化分析过程
|
||||||
|
- :material-eye: **深度理解**: 揭示隐藏的假设和思维盲点
|
||||||
|
- :material-lightbulb-on: **洞察提取**: 发现"原来如此"的时刻
|
||||||
|
- :material-rocket-launch: **行动导向**: 将深度理解转化为可执行步骤
|
||||||
|
- :material-theme-light-dark: **主题自适应**: 自动适配 OpenWebUI 深色/浅色主题
|
||||||
|
- :material-translate: **多语言**: 以用户偏好语言输出
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
1. 下载插件文件: [`deep_dive_cn.py`](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/deep-dive)
|
||||||
|
2. 上传到 OpenWebUI: **管理面板** → **设置** → **Functions**
|
||||||
|
3. 启用插件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
1. 在聊天中提供任何长文本、文章或会议记录
|
||||||
|
2. 点击消息操作栏中的 **精读** 按钮
|
||||||
|
3. 沿着视觉时间轴从"全景"探索到"路径"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置参数
|
||||||
|
|
||||||
|
| 选项 | 类型 | 默认值 | 描述 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `SHOW_STATUS` | boolean | `true` | 处理过程中是否显示状态更新 |
|
||||||
|
| `MODEL_ID` | string | `""` | 用于分析的 LLM 模型(空 = 当前模型) |
|
||||||
|
| `MIN_TEXT_LENGTH` | integer | `200` | 分析所需的最小文本长度 |
|
||||||
|
| `CLEAR_PREVIOUS_HTML` | boolean | `true` | 是否清除之前的插件结果 |
|
||||||
|
| `MESSAGE_COUNT` | integer | `1` | 要分析的最近消息数量 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 主题支持
|
||||||
|
|
||||||
|
精读插件自动适配 OpenWebUI 的深色/浅色主题:
|
||||||
|
|
||||||
|
- 从父文档 `<meta name="theme-color">` 标签检测主题
|
||||||
|
- 回退到 `html/body` 的 class 或 `data-theme` 属性
|
||||||
|
- 最后使用系统偏好 `prefers-color-scheme: dark`
|
||||||
|
|
||||||
|
!!! tip "最佳效果"
|
||||||
|
请在 OpenWebUI 中启用 **iframe Sandbox Allow Same Origin**:
|
||||||
|
**设置** → **界面** → **Artifacts** → 勾选 **iframe Sandbox Allow Same Origin**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 输出示例
|
||||||
|
|
||||||
|
插件生成精美的结构化时间轴:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 📖 精读分析报告 │
|
||||||
|
│ 👤 用户 📅 日期 📊 字数 │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ 🔍 阶段 01: 全景 (The Context) │
|
||||||
|
│ [高层级全景视图内容] │
|
||||||
|
│ │
|
||||||
|
│ 🧠 阶段 02: 脉络 (The Logic) │
|
||||||
|
│ • 推理结构分析... │
|
||||||
|
│ • 隐藏假设识别... │
|
||||||
|
│ │
|
||||||
|
│ 💎 阶段 03: 洞察 (The Insight) │
|
||||||
|
│ • 非显性价值提取... │
|
||||||
|
│ • 思维盲点揭示... │
|
||||||
|
│ │
|
||||||
|
│ 🚀 阶段 04: 路径 (The Path) │
|
||||||
|
│ ▸ 优先级行动 1... │
|
||||||
|
│ ▸ 优先级行动 2... │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 系统要求
|
||||||
|
|
||||||
|
!!! note "前提条件"
|
||||||
|
- OpenWebUI v0.3.0 或更高版本
|
||||||
|
- 使用当前活跃的 LLM 模型进行分析
|
||||||
|
- 需要 `markdown` Python 包
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 源代码
|
||||||
|
|
||||||
|
[:fontawesome-brands-github: 在 GitHub 上查看](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/deep-dive){ .md-button }
|
||||||
@@ -67,15 +67,15 @@ Actions are interactive plugins that:
|
|||||||
|
|
||||||
[:octicons-arrow-right-24: Documentation](export-to-word.md)
|
[:octicons-arrow-right-24: Documentation](export-to-word.md)
|
||||||
|
|
||||||
- :material-text-box-search:{ .lg .middle } **Summary**
|
- :material-brain:{ .lg .middle } **Deep Dive**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Generate concise summaries of long text content with key points extraction.
|
A comprehensive thinking lens that dives deep into any content - Context → Logic → Insight → Path. Supports theme auto-adaptation.
|
||||||
|
|
||||||
**Version:** 0.1.0
|
**Version:** 1.0.0
|
||||||
|
|
||||||
[:octicons-arrow-right-24: Documentation](summary.md)
|
[:octicons-arrow-right-24: Documentation](deep-dive.md)
|
||||||
|
|
||||||
- :material-image-text:{ .lg .middle } **Infographic to Markdown**
|
- :material-image-text:{ .lg .middle } **Infographic to Markdown**
|
||||||
|
|
||||||
|
|||||||
@@ -67,15 +67,15 @@ Actions 是交互式插件,能够:
|
|||||||
|
|
||||||
[:octicons-arrow-right-24: 查看文档](export-to-word.md)
|
[:octicons-arrow-right-24: 查看文档](export-to-word.md)
|
||||||
|
|
||||||
- :material-text-box-search:{ .lg .middle } **Summary**
|
- :material-brain:{ .lg .middle } **精读 (Deep Dive)**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
对长文本进行精简总结,提取要点。
|
全方位的思维透镜 —— 全景 → 脉络 → 洞察 → 路径。支持主题自适应。
|
||||||
|
|
||||||
**版本:** 0.1.0
|
**版本:** 1.0.0
|
||||||
|
|
||||||
[:octicons-arrow-right-24: 查看文档](summary.md)
|
[:octicons-arrow-right-24: 查看文档](deep-dive.zh.md)
|
||||||
|
|
||||||
- :material-image-text:{ .lg .middle } **信息图转 Markdown**
|
- :material-image-text:{ .lg .middle } **信息图转 Markdown**
|
||||||
|
|
||||||
|
|||||||
83
plugins/actions/deep-dive/README.md
Normal file
83
plugins/actions/deep-dive/README.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 🌊 Deep Dive
|
||||||
|
|
||||||
|
**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.0.0 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|
||||||
|
A comprehensive thinking lens that dives deep into any content - from context to logic, insights, and action paths.
|
||||||
|
|
||||||
|
## 🔥 What's New in v1.0.0
|
||||||
|
|
||||||
|
- ✨ **Thinking Chain Structure**: Moves from surface understanding to deep strategic action.
|
||||||
|
- 🔍 **Phase 01: The Context**: Panoramic view of the situation and background.
|
||||||
|
- 🧠 **Phase 02: The Logic**: Deconstruction of the underlying reasoning and mental models.
|
||||||
|
- 💎 **Phase 03: The Insight**: Extraction of non-obvious value and hidden implications.
|
||||||
|
- 🚀 **Phase 04: The Path**: Definition of specific, prioritized strategic directions.
|
||||||
|
- 🎨 **Premium UI**: Modern, process-oriented design with a "Thinking Line" timeline.
|
||||||
|
- 🌗 **Theme Adaptive**: Automatically adapts to OpenWebUI's light/dark theme.
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
|
||||||
|
- 🌊 **Deep Thinking**: Not just a summary, but a full deconstruction of content.
|
||||||
|
- 🧠 **Logical Analysis**: Reveals how arguments are built and identifies hidden assumptions.
|
||||||
|
- 💎 **Value Extraction**: Finds the "Aha!" moments and blind spots.
|
||||||
|
- 🚀 **Action Oriented**: Translates deep understanding into immediate, actionable steps.
|
||||||
|
- 🌍 **Multi-language**: Automatically adapts to the user's preferred language.
|
||||||
|
- 🌗 **Theme Support**: Seamlessly switches between light and dark themes based on OpenWebUI settings.
|
||||||
|
|
||||||
|
## 🚀 How to Use
|
||||||
|
|
||||||
|
1. **Input Content**: Provide any text, article, or meeting notes in the chat.
|
||||||
|
2. **Trigger Deep Dive**: Click the **Deep Dive** action button.
|
||||||
|
3. **Explore the Chain**: Follow the visual timeline from Context to Path.
|
||||||
|
|
||||||
|
## ⚙️ Configuration (Valves)
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Show Status (SHOW_STATUS)** | `True` | Whether to show status updates during the thinking process. |
|
||||||
|
| **Model ID (MODEL_ID)** | `Empty` | LLM model for analysis. Empty = use current model. |
|
||||||
|
| **Min Text Length (MIN_TEXT_LENGTH)** | `200` | Minimum characters required for a meaningful deep dive. |
|
||||||
|
| **Clear Previous HTML (CLEAR_PREVIOUS_HTML)** | `True` | Whether to clear previous plugin results. |
|
||||||
|
| **Message Count (MESSAGE_COUNT)** | `1` | Number of recent messages to analyze. |
|
||||||
|
|
||||||
|
## 🌗 Theme Support
|
||||||
|
|
||||||
|
The plugin automatically detects and adapts to OpenWebUI's theme settings:
|
||||||
|
|
||||||
|
- **Detection Priority**:
|
||||||
|
1. Parent document `<meta name="theme-color">` tag
|
||||||
|
2. Parent document `html/body` class or `data-theme` attribute
|
||||||
|
3. System preference via `prefers-color-scheme: dark`
|
||||||
|
|
||||||
|
- **Requirements**: For best results, enable **iframe Sandbox Allow Same Origin** in OpenWebUI:
|
||||||
|
- Go to **Settings** → **Interface** → **Artifacts** → Check **iframe Sandbox Allow Same Origin**
|
||||||
|
|
||||||
|
## 🎨 Visual Preview
|
||||||
|
|
||||||
|
The plugin generates a structured thinking timeline:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 🌊 Deep Dive Analysis │
|
||||||
|
│ 👤 User 📅 Date 📊 Word count │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ 🔍 Phase 01: The Context │
|
||||||
|
│ [High-level panoramic view] │
|
||||||
|
│ │
|
||||||
|
│ 🧠 Phase 02: The Logic │
|
||||||
|
│ • Reasoning structure... │
|
||||||
|
│ • Hidden assumptions... │
|
||||||
|
│ │
|
||||||
|
│ 💎 Phase 03: The Insight │
|
||||||
|
│ • Non-obvious value... │
|
||||||
|
│ • Blind spots revealed... │
|
||||||
|
│ │
|
||||||
|
│ 🚀 Phase 04: The Path │
|
||||||
|
│ ▸ Priority Action 1... │
|
||||||
|
│ ▸ Priority Action 2... │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📂 Files
|
||||||
|
|
||||||
|
- `deep_dive.py` - English version
|
||||||
|
- `deep_dive_cn.py` - Chinese version (精读)
|
||||||
83
plugins/actions/deep-dive/README_CN.md
Normal file
83
plugins/actions/deep-dive/README_CN.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 📖 精读
|
||||||
|
|
||||||
|
**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 1.0.0 | **项目:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)
|
||||||
|
|
||||||
|
全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。
|
||||||
|
|
||||||
|
## 🔥 v1.0.0 更新内容
|
||||||
|
|
||||||
|
- ✨ **思维链结构**: 从表面理解一步步深入到战略行动。
|
||||||
|
- 🔍 **阶段 01: 全景 (The Context)**: 提供情境与背景的高层级全景视图。
|
||||||
|
- 🧠 **阶段 02: 脉络 (The Logic)**: 解构底层推理逻辑与思维模型。
|
||||||
|
- 💎 **阶段 03: 洞察 (The Insight)**: 提取非显性价值与隐藏的深层含义。
|
||||||
|
- 🚀 **阶段 04: 路径 (The Path)**: 定义具体的、按优先级排列的战略方向。
|
||||||
|
- 🎨 **高端 UI**: 现代化的过程导向设计,带有"思维导火索"时间轴。
|
||||||
|
- 🌗 **主题自适应**: 自动适配 OpenWebUI 的深色/浅色主题。
|
||||||
|
|
||||||
|
## ✨ 核心特性
|
||||||
|
|
||||||
|
- 📖 **深度思考**: 不仅仅是摘要,而是对内容的全面解构。
|
||||||
|
- 🧠 **逻辑分析**: 揭示论点是如何构建的,识别隐藏的假设。
|
||||||
|
- 💎 **价值提取**: 发现"原来如此"的时刻与思维盲点。
|
||||||
|
- 🚀 **行动导向**: 将深度理解转化为立即、可执行的步骤。
|
||||||
|
- 🌍 **多语言支持**: 自动适配用户的偏好语言。
|
||||||
|
- 🌗 **主题支持**: 根据 OpenWebUI 设置自动切换深色/浅色主题。
|
||||||
|
|
||||||
|
## 🚀 如何使用
|
||||||
|
|
||||||
|
1. **输入内容**: 在聊天中提供任何文本、文章或会议记录。
|
||||||
|
2. **触发精读**: 点击 **精读** 操作按钮。
|
||||||
|
3. **探索思维链**: 沿着视觉时间轴从"全景"探索到"路径"。
|
||||||
|
|
||||||
|
## ⚙️ 配置参数 (Valves)
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 描述 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **显示状态 (SHOW_STATUS)** | `True` | 是否在思维过程中显示状态更新。 |
|
||||||
|
| **模型 ID (MODEL_ID)** | `空` | 用于分析的 LLM 模型。留空 = 使用当前模型。 |
|
||||||
|
| **最小文本长度 (MIN_TEXT_LENGTH)** | `200` | 进行有意义的精读所需的最小字符数。 |
|
||||||
|
| **清除旧 HTML (CLEAR_PREVIOUS_HTML)** | `True` | 是否清除之前的插件结果。 |
|
||||||
|
| **消息数量 (MESSAGE_COUNT)** | `1` | 要分析的最近消息数量。 |
|
||||||
|
|
||||||
|
## 🌗 主题支持
|
||||||
|
|
||||||
|
插件会自动检测并适配 OpenWebUI 的主题设置:
|
||||||
|
|
||||||
|
- **检测优先级**:
|
||||||
|
1. 父文档 `<meta name="theme-color">` 标签
|
||||||
|
2. 父文档 `html/body` 的 class 或 `data-theme` 属性
|
||||||
|
3. 系统偏好 `prefers-color-scheme: dark`
|
||||||
|
|
||||||
|
- **环境要求**: 为获得最佳效果,请在 OpenWebUI 中启用 **iframe Sandbox Allow Same Origin**:
|
||||||
|
- 进入 **设置** → **界面** → **Artifacts** → 勾选 **iframe Sandbox Allow Same Origin**
|
||||||
|
|
||||||
|
## 🎨 视觉预览
|
||||||
|
|
||||||
|
插件生成结构化的思维时间轴:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 📖 精读分析报告 │
|
||||||
|
│ 👤 用户 📅 日期 📊 字数 │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ 🔍 阶段 01: 全景 (The Context) │
|
||||||
|
│ [高层级全景视图内容] │
|
||||||
|
│ │
|
||||||
|
│ 🧠 阶段 02: 脉络 (The Logic) │
|
||||||
|
│ • 推理结构分析... │
|
||||||
|
│ • 隐藏假设识别... │
|
||||||
|
│ │
|
||||||
|
│ 💎 阶段 03: 洞察 (The Insight) │
|
||||||
|
│ • 非显性价值提取... │
|
||||||
|
│ • 思维盲点揭示... │
|
||||||
|
│ │
|
||||||
|
│ 🚀 阶段 04: 路径 (The Path) │
|
||||||
|
│ ▸ 优先级行动 1... │
|
||||||
|
│ ▸ 优先级行动 2... │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📂 文件说明
|
||||||
|
|
||||||
|
- `deep_dive.py` - 英文版 (Deep Dive)
|
||||||
|
- `deep_dive_cn.py` - 中文版 (精读)
|
||||||
BIN
plugins/actions/deep-dive/deep_dive.png
Normal file
BIN
plugins/actions/deep-dive/deep_dive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 783 KiB |
884
plugins/actions/deep-dive/deep_dive.py
Normal file
884
plugins/actions/deep-dive/deep_dive.py
Normal file
@@ -0,0 +1,884 @@
|
|||||||
|
"""
|
||||||
|
title: Deep Dive
|
||||||
|
author: Fu-Jie
|
||||||
|
author_url: https://github.com/Fu-Jie
|
||||||
|
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
|
version: 1.0.0
|
||||||
|
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xMiA3djE0Ii8+PHBhdGggZD0iTTMgMThhMSAxIDAgMCAxLTEtMVY0YTEgMSAwIDAgMSAxLTFoNWE0IDQgMCAwIDEgNCA0IDQgNCAwIDAgMSA0LTRoNWExIDEgMCAwIDEgMSAxdjEzYTEgMSAwIDAgMS0xIDFoLTZhMyAzIDAgMCAwLTMgMyAzIDMgMCAwIDAtMy0zeiIvPjxwYXRoIGQ9Ik02IDEyaDIiLz48cGF0aCBkPSJNMTYgMTJoMiIvPjwvc3ZnPg==
|
||||||
|
requirements: markdown
|
||||||
|
description: A comprehensive thinking lens that dives deep into any content - from context to logic, insights, and action paths.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Standard library imports
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any, Callable, Awaitable
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from fastapi import Request
|
||||||
|
import markdown
|
||||||
|
|
||||||
|
# OpenWebUI imports
|
||||||
|
from open_webui.utils.chat import generate_chat_completion
|
||||||
|
from open_webui.models.users import Users
|
||||||
|
|
||||||
|
# Logging setup
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# HTML Template - Process-Oriented Design with Theme Support
|
||||||
|
# =================================================================
|
||||||
|
HTML_WRAPPER_TEMPLATE = """
|
||||||
|
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{user_language}">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--dd-bg-primary: #ffffff;
|
||||||
|
--dd-bg-secondary: #f8fafc;
|
||||||
|
--dd-bg-tertiary: #f1f5f9;
|
||||||
|
--dd-text-primary: #0f172a;
|
||||||
|
--dd-text-secondary: #334155;
|
||||||
|
--dd-text-dim: #64748b;
|
||||||
|
--dd-border: #e2e8f0;
|
||||||
|
--dd-accent: #3b82f6;
|
||||||
|
--dd-accent-soft: #eff6ff;
|
||||||
|
--dd-header-gradient: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||||
|
--dd-shadow: 0 10px 40px rgba(0,0,0,0.06);
|
||||||
|
--dd-code-bg: #f1f5f9;
|
||||||
|
}
|
||||||
|
.theme-dark {
|
||||||
|
--dd-bg-primary: #1e293b;
|
||||||
|
--dd-bg-secondary: #0f172a;
|
||||||
|
--dd-bg-tertiary: #334155;
|
||||||
|
--dd-text-primary: #f1f5f9;
|
||||||
|
--dd-text-secondary: #e2e8f0;
|
||||||
|
--dd-text-dim: #94a3b8;
|
||||||
|
--dd-border: #475569;
|
||||||
|
--dd-accent: #60a5fa;
|
||||||
|
--dd-accent-soft: rgba(59, 130, 246, 0.15);
|
||||||
|
--dd-header-gradient: linear-gradient(135deg, #0f172a 0%, #1e1e2e 100%);
|
||||||
|
--dd-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||||||
|
--dd-code-bg: #334155;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
#main-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.plugin-item {
|
||||||
|
background: var(--dd-bg-primary);
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: var(--dd-shadow);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--dd-border);
|
||||||
|
}
|
||||||
|
/* STYLES_INSERTION_POINT */
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main-container">
|
||||||
|
<!-- CONTENT_INSERTION_POINT -->
|
||||||
|
</div>
|
||||||
|
<!-- SCRIPTS_INSERTION_POINT -->
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const parseColorLuma = (colorStr) => {
|
||||||
|
if (!colorStr) return null;
|
||||||
|
let m = colorStr.match(/^#?([0-9a-f]{6})$/i);
|
||||||
|
if (m) {
|
||||||
|
const hex = m[1];
|
||||||
|
const r = parseInt(hex.slice(0, 2), 16);
|
||||||
|
const g = parseInt(hex.slice(2, 4), 16);
|
||||||
|
const b = parseInt(hex.slice(4, 6), 16);
|
||||||
|
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
||||||
|
}
|
||||||
|
m = colorStr.match(/rgba?\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)/i);
|
||||||
|
if (m) {
|
||||||
|
const r = parseInt(m[1], 10);
|
||||||
|
const g = parseInt(m[2], 10);
|
||||||
|
const b = parseInt(m[3], 10);
|
||||||
|
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const getThemeFromMeta = (doc) => {
|
||||||
|
const metas = Array.from((doc || document).querySelectorAll('meta[name="theme-color"]'));
|
||||||
|
if (!metas.length) return null;
|
||||||
|
const color = metas[metas.length - 1].content.trim();
|
||||||
|
const luma = parseColorLuma(color);
|
||||||
|
if (luma === null) return null;
|
||||||
|
return luma < 0.5 ? 'dark' : 'light';
|
||||||
|
};
|
||||||
|
const getParentDocumentSafe = () => {
|
||||||
|
try {
|
||||||
|
if (!window.parent || window.parent === window) return null;
|
||||||
|
const pDoc = window.parent.document;
|
||||||
|
void pDoc.title;
|
||||||
|
return pDoc;
|
||||||
|
} catch (err) { return null; }
|
||||||
|
};
|
||||||
|
const getThemeFromParentClass = () => {
|
||||||
|
try {
|
||||||
|
if (!window.parent || window.parent === window) return null;
|
||||||
|
const pDoc = window.parent.document;
|
||||||
|
const html = pDoc.documentElement;
|
||||||
|
const body = pDoc.body;
|
||||||
|
const htmlClass = html ? html.className : '';
|
||||||
|
const bodyClass = body ? body.className : '';
|
||||||
|
const htmlDataTheme = html ? html.getAttribute('data-theme') : '';
|
||||||
|
if (htmlDataTheme === 'dark' || bodyClass.includes('dark') || htmlClass.includes('dark')) return 'dark';
|
||||||
|
if (htmlDataTheme === 'light' || bodyClass.includes('light') || htmlClass.includes('light')) return 'light';
|
||||||
|
return null;
|
||||||
|
} catch (err) { return null; }
|
||||||
|
};
|
||||||
|
const setTheme = () => {
|
||||||
|
const parentDoc = getParentDocumentSafe();
|
||||||
|
const metaTheme = parentDoc ? getThemeFromMeta(parentDoc) : null;
|
||||||
|
const parentClassTheme = getThemeFromParentClass();
|
||||||
|
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
const chosen = metaTheme || parentClassTheme || (prefersDark ? 'dark' : 'light');
|
||||||
|
document.documentElement.classList.toggle('theme-dark', chosen === 'dark');
|
||||||
|
};
|
||||||
|
setTheme();
|
||||||
|
if (window.matchMedia) {
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', setTheme);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# LLM Prompts - Deep Dive Thinking Chain
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = """
|
||||||
|
You are a Deep Dive Analyst. Your goal is to guide the user through a comprehensive thinking process, moving from surface understanding to deep strategic action.
|
||||||
|
|
||||||
|
## Thinking Structure (STRICT)
|
||||||
|
|
||||||
|
You MUST analyze the input across these four specific dimensions:
|
||||||
|
|
||||||
|
### 1. 🔍 The Context (What?)
|
||||||
|
Provide a high-level panoramic view. What is this content about? What is the core situation, background, or problem being addressed? (2-3 paragraphs)
|
||||||
|
|
||||||
|
### 2. 🧠 The Logic (Why?)
|
||||||
|
Deconstruct the underlying structure. How is the argument built? What is the reasoning, the hidden assumptions, or the mental models at play? (Bullet points)
|
||||||
|
|
||||||
|
### 3. 💎 The Insight (So What?)
|
||||||
|
Extract the non-obvious value. What are the "Aha!" moments? What are the implications, the blind spots, or the unique perspectives revealed? (Bullet points)
|
||||||
|
|
||||||
|
### 4. 🚀 The Path (Now What?)
|
||||||
|
Define the strategic direction. What are the specific, prioritized next steps? How can this knowledge be applied immediately? (Actionable steps)
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
- Output in the user's specified language.
|
||||||
|
- Maintain a professional, analytical, yet inspiring tone.
|
||||||
|
- Focus on the *process* of understanding, not just the result.
|
||||||
|
- No greetings or meta-commentary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
USER_PROMPT = """
|
||||||
|
Initiate a Deep Dive into the following content:
|
||||||
|
|
||||||
|
**User Context:**
|
||||||
|
- User: {user_name}
|
||||||
|
- Time: {current_date_time_str}
|
||||||
|
- Language: {user_language}
|
||||||
|
|
||||||
|
**Content to Analyze:**
|
||||||
|
```
|
||||||
|
{long_text_content}
|
||||||
|
```
|
||||||
|
|
||||||
|
Please execute the full thinking chain: Context → Logic → Insight → Path.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# Premium CSS Design - Deep Dive Theme
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
CSS_TEMPLATE = """
|
||||||
|
.deep-dive {
|
||||||
|
font-family: 'Inter', -apple-system, system-ui, sans-serif;
|
||||||
|
color: var(--dd-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-header {
|
||||||
|
background: var(--dd-header-gradient);
|
||||||
|
padding: 40px 32px;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-header-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
border-radius: 100px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-body {
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 40px;
|
||||||
|
position: relative;
|
||||||
|
background: var(--dd-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The Thinking Line */
|
||||||
|
.dd-body::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 52px;
|
||||||
|
top: 40px;
|
||||||
|
bottom: 40px;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--dd-border);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-step {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-step-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: var(--dd-bg-primary);
|
||||||
|
border: 2px solid var(--dd-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-step:hover .dd-step-icon {
|
||||||
|
border-color: var(--dd-accent);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-step-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-step-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--dd-accent);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-step-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--dd-text-primary);
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-text {
|
||||||
|
line-height: 1.7;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-text p { margin-bottom: 16px; }
|
||||||
|
.dd-text p:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.dd-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-list-item {
|
||||||
|
background: var(--dd-bg-secondary);
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border-left: 4px solid var(--dd-border);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-list-item:hover {
|
||||||
|
background: var(--dd-bg-tertiary);
|
||||||
|
border-left-color: var(--dd-accent);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-list-item strong {
|
||||||
|
color: var(--dd-text-primary);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-path-item {
|
||||||
|
background: var(--dd-accent-soft);
|
||||||
|
border-left-color: var(--dd-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-footer {
|
||||||
|
padding: 24px 32px;
|
||||||
|
background: var(--dd-bg-secondary);
|
||||||
|
border-top: 1px solid var(--dd-border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--dd-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-tag {
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: var(--dd-bg-tertiary);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-text code,
|
||||||
|
.dd-list-item code {
|
||||||
|
background: var(--dd-code-bg);
|
||||||
|
color: var(--dd-text-primary);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-list-item em {
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--dd-text-dim);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
CONTENT_TEMPLATE = """
|
||||||
|
<div class="deep-dive">
|
||||||
|
<div class="dd-header">
|
||||||
|
<div class="dd-header-badge">Thinking Process</div>
|
||||||
|
<h1 class="dd-title">Deep Dive Analysis</h1>
|
||||||
|
<div class="dd-meta">
|
||||||
|
<span>👤 {user_name}</span>
|
||||||
|
<span>📅 {current_date_time_str}</span>
|
||||||
|
<span>📊 {word_count} words</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dd-body">
|
||||||
|
<!-- Step 1: Context -->
|
||||||
|
<div class="dd-step">
|
||||||
|
<div class="dd-step-icon">🔍</div>
|
||||||
|
<div class="dd-step-content">
|
||||||
|
<div class="dd-step-label">Phase 01</div>
|
||||||
|
<h2 class="dd-step-title">The Context</h2>
|
||||||
|
<div class="dd-text">{context_html}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: Logic -->
|
||||||
|
<div class="dd-step">
|
||||||
|
<div class="dd-step-icon">🧠</div>
|
||||||
|
<div class="dd-step-content">
|
||||||
|
<div class="dd-step-label">Phase 02</div>
|
||||||
|
<h2 class="dd-step-title">The Logic</h2>
|
||||||
|
<div class="dd-text">{logic_html}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: Insight -->
|
||||||
|
<div class="dd-step">
|
||||||
|
<div class="dd-step-icon">💎</div>
|
||||||
|
<div class="dd-step-content">
|
||||||
|
<div class="dd-step-label">Phase 03</div>
|
||||||
|
<h2 class="dd-step-title">The Insight</h2>
|
||||||
|
<div class="dd-text">{insight_html}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 4: Path -->
|
||||||
|
<div class="dd-step">
|
||||||
|
<div class="dd-step-icon">🚀</div>
|
||||||
|
<div class="dd-step-content">
|
||||||
|
<div class="dd-step-label">Phase 04</div>
|
||||||
|
<h2 class="dd-step-title">The Path</h2>
|
||||||
|
<div class="dd-text">{path_html}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dd-footer">
|
||||||
|
<span>Deep Dive Engine v1.0</span>
|
||||||
|
<span><span class="dd-tag">AI-Powered</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Action:
|
||||||
|
class Valves(BaseModel):
|
||||||
|
SHOW_STATUS: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Whether to show operation status updates.",
|
||||||
|
)
|
||||||
|
MODEL_ID: str = Field(
|
||||||
|
default="",
|
||||||
|
description="LLM Model ID for analysis. Empty = use current model.",
|
||||||
|
)
|
||||||
|
MIN_TEXT_LENGTH: int = Field(
|
||||||
|
default=200,
|
||||||
|
description="Minimum text length for deep dive (chars).",
|
||||||
|
)
|
||||||
|
CLEAR_PREVIOUS_HTML: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="Whether to clear previous plugin results.",
|
||||||
|
)
|
||||||
|
MESSAGE_COUNT: int = Field(
|
||||||
|
default=1,
|
||||||
|
description="Number of recent messages to analyze.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.valves = self.Valves()
|
||||||
|
|
||||||
|
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||||
|
"""Safely extracts user context information."""
|
||||||
|
if isinstance(__user__, (list, tuple)):
|
||||||
|
user_data = __user__[0] if __user__ else {}
|
||||||
|
elif isinstance(__user__, dict):
|
||||||
|
user_data = __user__
|
||||||
|
else:
|
||||||
|
user_data = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user_id": user_data.get("id", "unknown_user"),
|
||||||
|
"user_name": user_data.get("name", "User"),
|
||||||
|
"user_language": user_data.get("language", "en-US"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _process_llm_output(self, llm_output: str) -> Dict[str, str]:
|
||||||
|
"""Parse LLM output and convert to styled HTML."""
|
||||||
|
# Extract sections using flexible regex
|
||||||
|
context_match = re.search(
|
||||||
|
r"###\s*1\.\s*🔍?\s*The Context\s*\((.*?)\)\s*\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
logic_match = re.search(
|
||||||
|
r"###\s*2\.\s*🧠?\s*The Logic\s*\((.*?)\)\s*\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
insight_match = re.search(
|
||||||
|
r"###\s*3\.\s*💎?\s*The Insight\s*\((.*?)\)\s*\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
path_match = re.search(
|
||||||
|
r"###\s*4\.\s*🚀?\s*The Path\s*\((.*?)\)\s*\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fallback if numbering is different
|
||||||
|
if not context_match:
|
||||||
|
context_match = re.search(
|
||||||
|
r"###\s*🔍?\s*The Context.*?\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if not logic_match:
|
||||||
|
logic_match = re.search(
|
||||||
|
r"###\s*🧠?\s*The Logic.*?\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if not insight_match:
|
||||||
|
insight_match = re.search(
|
||||||
|
r"###\s*💎?\s*The Insight.*?\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if not path_match:
|
||||||
|
path_match = re.search(
|
||||||
|
r"###\s*🚀?\s*The Path.*?\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
context_md = (
|
||||||
|
context_match.group(1 if context_match.lastindex == 1 else 2).strip()
|
||||||
|
if context_match
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
logic_md = (
|
||||||
|
logic_match.group(1 if logic_match.lastindex == 1 else 2).strip()
|
||||||
|
if logic_match
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
insight_md = (
|
||||||
|
insight_match.group(1 if insight_match.lastindex == 1 else 2).strip()
|
||||||
|
if insight_match
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
path_md = (
|
||||||
|
path_match.group(1 if path_match.lastindex == 1 else 2).strip()
|
||||||
|
if path_match
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
if not any([context_md, logic_md, insight_md, path_md]):
|
||||||
|
context_md = llm_output.strip()
|
||||||
|
logger.warning("LLM output did not follow format. Using as context.")
|
||||||
|
|
||||||
|
md_extensions = ["nl2br"]
|
||||||
|
|
||||||
|
context_html = (
|
||||||
|
markdown.markdown(context_md, extensions=md_extensions)
|
||||||
|
if context_md
|
||||||
|
else '<p class="dd-no-content">No context extracted.</p>'
|
||||||
|
)
|
||||||
|
logic_html = (
|
||||||
|
self._process_list_items(logic_md, "logic")
|
||||||
|
if logic_md
|
||||||
|
else '<p class="dd-no-content">No logic deconstructed.</p>'
|
||||||
|
)
|
||||||
|
insight_html = (
|
||||||
|
self._process_list_items(insight_md, "insight")
|
||||||
|
if insight_md
|
||||||
|
else '<p class="dd-no-content">No insights found.</p>'
|
||||||
|
)
|
||||||
|
path_html = (
|
||||||
|
self._process_list_items(path_md, "path")
|
||||||
|
if path_md
|
||||||
|
else '<p class="dd-no-content">No path defined.</p>'
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"context_html": context_html,
|
||||||
|
"logic_html": logic_html,
|
||||||
|
"insight_html": insight_html,
|
||||||
|
"path_html": path_html,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _process_list_items(self, md_content: str, section_type: str) -> str:
|
||||||
|
"""Convert markdown list to styled HTML cards with full markdown support."""
|
||||||
|
lines = md_content.strip().split("\n")
|
||||||
|
items = []
|
||||||
|
current_paragraph = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# Check for list item (bullet or numbered)
|
||||||
|
bullet_match = re.match(r"^[-*]\s+(.+)$", line)
|
||||||
|
numbered_match = re.match(r"^\d+\.\s+(.+)$", line)
|
||||||
|
|
||||||
|
if bullet_match or numbered_match:
|
||||||
|
# Flush any accumulated paragraph
|
||||||
|
if current_paragraph:
|
||||||
|
para_text = " ".join(current_paragraph)
|
||||||
|
para_html = self._convert_inline_markdown(para_text)
|
||||||
|
items.append(f"<p>{para_html}</p>")
|
||||||
|
current_paragraph = []
|
||||||
|
|
||||||
|
# Extract the list item content
|
||||||
|
text = (
|
||||||
|
bullet_match.group(1) if bullet_match else numbered_match.group(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle bold title pattern: **Title:** Description or **Title**: Description
|
||||||
|
title_match = re.match(r"\*\*(.+?)\*\*[:\s]*(.*)$", text)
|
||||||
|
if title_match:
|
||||||
|
title = self._convert_inline_markdown(title_match.group(1))
|
||||||
|
desc = self._convert_inline_markdown(title_match.group(2).strip())
|
||||||
|
path_class = "dd-path-item" if section_type == "path" else ""
|
||||||
|
item_html = f'<div class="dd-list-item {path_class}"><strong>{title}</strong>{desc}</div>'
|
||||||
|
else:
|
||||||
|
text_html = self._convert_inline_markdown(text)
|
||||||
|
path_class = "dd-path-item" if section_type == "path" else ""
|
||||||
|
item_html = (
|
||||||
|
f'<div class="dd-list-item {path_class}">{text_html}</div>'
|
||||||
|
)
|
||||||
|
items.append(item_html)
|
||||||
|
elif line and not line.startswith("#"):
|
||||||
|
# Accumulate paragraph text
|
||||||
|
current_paragraph.append(line)
|
||||||
|
elif not line and current_paragraph:
|
||||||
|
# Empty line ends paragraph
|
||||||
|
para_text = " ".join(current_paragraph)
|
||||||
|
para_html = self._convert_inline_markdown(para_text)
|
||||||
|
items.append(f"<p>{para_html}</p>")
|
||||||
|
current_paragraph = []
|
||||||
|
|
||||||
|
# Flush remaining paragraph
|
||||||
|
if current_paragraph:
|
||||||
|
para_text = " ".join(current_paragraph)
|
||||||
|
para_html = self._convert_inline_markdown(para_text)
|
||||||
|
items.append(f"<p>{para_html}</p>")
|
||||||
|
|
||||||
|
if items:
|
||||||
|
return f'<div class="dd-list">{" ".join(items)}</div>'
|
||||||
|
return f'<p class="dd-no-content">No items found.</p>'
|
||||||
|
|
||||||
|
def _convert_inline_markdown(self, text: str) -> str:
|
||||||
|
"""Convert inline markdown (bold, italic, code) to HTML."""
|
||||||
|
# Convert inline code: `code` -> <code>code</code>
|
||||||
|
text = re.sub(r"`([^`]+)`", r"<code>\1</code>", text)
|
||||||
|
# Convert bold: **text** -> <strong>text</strong>
|
||||||
|
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
|
||||||
|
# Convert italic: *text* -> <em>text</em> (but not inside **)
|
||||||
|
text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
async def _emit_status(
|
||||||
|
self,
|
||||||
|
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||||
|
description: str,
|
||||||
|
done: bool = False,
|
||||||
|
):
|
||||||
|
"""Emits a status update event."""
|
||||||
|
if self.valves.SHOW_STATUS and emitter:
|
||||||
|
await emitter(
|
||||||
|
{"type": "status", "data": {"description": description, "done": done}}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _emit_notification(
|
||||||
|
self,
|
||||||
|
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||||
|
content: str,
|
||||||
|
ntype: str = "info",
|
||||||
|
):
|
||||||
|
"""Emits a notification event."""
|
||||||
|
if emitter:
|
||||||
|
await emitter(
|
||||||
|
{"type": "notification", "data": {"type": ntype, "content": content}}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _remove_existing_html(self, content: str) -> str:
|
||||||
|
"""Removes existing plugin-generated HTML."""
|
||||||
|
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."""
|
||||||
|
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 ""
|
||||||
|
|
||||||
|
def _merge_html(
|
||||||
|
self,
|
||||||
|
existing_html: str,
|
||||||
|
new_content: str,
|
||||||
|
new_styles: str = "",
|
||||||
|
user_language: str = "en-US",
|
||||||
|
) -> str:
|
||||||
|
"""Merges new content into HTML container."""
|
||||||
|
if "<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html:
|
||||||
|
base_html = re.sub(r"^```html\s*", "", existing_html)
|
||||||
|
base_html = re.sub(r"\s*```$", "", base_html)
|
||||||
|
else:
|
||||||
|
base_html = HTML_WRAPPER_TEMPLATE.replace("{user_language}", user_language)
|
||||||
|
|
||||||
|
wrapped = f'<div class="plugin-item">\n{new_content}\n</div>'
|
||||||
|
|
||||||
|
if new_styles:
|
||||||
|
base_html = base_html.replace(
|
||||||
|
"/* STYLES_INSERTION_POINT */",
|
||||||
|
f"{new_styles}\n/* STYLES_INSERTION_POINT */",
|
||||||
|
)
|
||||||
|
|
||||||
|
base_html = base_html.replace(
|
||||||
|
"<!-- CONTENT_INSERTION_POINT -->",
|
||||||
|
f"{wrapped}\n<!-- CONTENT_INSERTION_POINT -->",
|
||||||
|
)
|
||||||
|
|
||||||
|
return base_html.strip()
|
||||||
|
|
||||||
|
def _build_content_html(self, context: dict) -> str:
|
||||||
|
"""Build content HTML."""
|
||||||
|
html = CONTENT_TEMPLATE
|
||||||
|
for key, value in context.items():
|
||||||
|
html = html.replace(f"{{{key}}}", str(value))
|
||||||
|
return html
|
||||||
|
|
||||||
|
async def action(
|
||||||
|
self,
|
||||||
|
body: dict,
|
||||||
|
__user__: Optional[Dict[str, Any]] = None,
|
||||||
|
__event_emitter__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||||
|
__request__: Optional[Request] = None,
|
||||||
|
) -> Optional[dict]:
|
||||||
|
logger.info("Action: Deep Dive v1.0.0 started")
|
||||||
|
|
||||||
|
user_ctx = self._get_user_context(__user__)
|
||||||
|
user_id = user_ctx["user_id"]
|
||||||
|
user_name = user_ctx["user_name"]
|
||||||
|
user_language = user_ctx["user_language"]
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
current_date_time_str = now.strftime("%b %d, %Y %H:%M")
|
||||||
|
|
||||||
|
original_content = ""
|
||||||
|
try:
|
||||||
|
messages = body.get("messages", [])
|
||||||
|
if not messages:
|
||||||
|
raise ValueError("No messages found.")
|
||||||
|
|
||||||
|
message_count = min(self.valves.MESSAGE_COUNT, len(messages))
|
||||||
|
recent_messages = messages[-message_count:]
|
||||||
|
|
||||||
|
aggregated_parts = []
|
||||||
|
for msg in recent_messages:
|
||||||
|
text = self._extract_text_content(msg.get("content"))
|
||||||
|
if text:
|
||||||
|
aggregated_parts.append(text)
|
||||||
|
|
||||||
|
if not aggregated_parts:
|
||||||
|
raise ValueError("No text content found.")
|
||||||
|
|
||||||
|
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||||
|
word_count = len(original_content.split())
|
||||||
|
|
||||||
|
if len(original_content) < self.valves.MIN_TEXT_LENGTH:
|
||||||
|
msg = f"Content too brief ({len(original_content)} chars). Deep Dive requires at least {self.valves.MIN_TEXT_LENGTH} chars for meaningful analysis."
|
||||||
|
await self._emit_notification(__event_emitter__, msg, "warning")
|
||||||
|
return {"messages": [{"role": "assistant", "content": f"⚠️ {msg}"}]}
|
||||||
|
|
||||||
|
await self._emit_notification(
|
||||||
|
__event_emitter__, "🌊 Initiating Deep Dive thinking process...", "info"
|
||||||
|
)
|
||||||
|
await self._emit_status(
|
||||||
|
__event_emitter__, "🌊 Deep Dive: Analyzing Context & Logic...", False
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = USER_PROMPT.format(
|
||||||
|
user_name=user_name,
|
||||||
|
current_date_time_str=current_date_time_str,
|
||||||
|
user_language=user_language,
|
||||||
|
long_text_content=original_content,
|
||||||
|
)
|
||||||
|
|
||||||
|
model = self.valves.MODEL_ID or body.get("model")
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": SYSTEM_PROMPT},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
"stream": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
user_obj = Users.get_user_by_id(user_id)
|
||||||
|
if not user_obj:
|
||||||
|
raise ValueError(f"User not found: {user_id}")
|
||||||
|
|
||||||
|
response = await generate_chat_completion(__request__, payload, user_obj)
|
||||||
|
llm_output = response["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
processed = self._process_llm_output(llm_output)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"user_name": user_name,
|
||||||
|
"current_date_time_str": current_date_time_str,
|
||||||
|
"word_count": word_count,
|
||||||
|
**processed,
|
||||||
|
}
|
||||||
|
|
||||||
|
content_html = self._build_content_html(context)
|
||||||
|
|
||||||
|
# Handle existing HTML
|
||||||
|
existing = ""
|
||||||
|
match = re.search(
|
||||||
|
r"```html\s*(<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?)```",
|
||||||
|
original_content,
|
||||||
|
)
|
||||||
|
if match:
|
||||||
|
existing = match.group(1)
|
||||||
|
|
||||||
|
if self.valves.CLEAR_PREVIOUS_HTML or not existing:
|
||||||
|
original_content = self._remove_existing_html(original_content)
|
||||||
|
final_html = self._merge_html(
|
||||||
|
"", content_html, CSS_TEMPLATE, user_language
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
original_content = self._remove_existing_html(original_content)
|
||||||
|
final_html = self._merge_html(
|
||||||
|
existing, content_html, CSS_TEMPLATE, user_language
|
||||||
|
)
|
||||||
|
|
||||||
|
body["messages"][-1][
|
||||||
|
"content"
|
||||||
|
] = f"{original_content}\n\n```html\n{final_html}\n```"
|
||||||
|
|
||||||
|
await self._emit_status(__event_emitter__, "🌊 Deep Dive complete!", True)
|
||||||
|
await self._emit_notification(
|
||||||
|
__event_emitter__,
|
||||||
|
f"🌊 Deep Dive complete, {user_name}! Thinking chain generated.",
|
||||||
|
"success",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Deep Dive Error: {e}", exc_info=True)
|
||||||
|
body["messages"][-1][
|
||||||
|
"content"
|
||||||
|
] = f"{original_content}\n\n❌ **Error:** {str(e)}"
|
||||||
|
await self._emit_status(__event_emitter__, "Deep Dive failed.", True)
|
||||||
|
await self._emit_notification(
|
||||||
|
__event_emitter__, f"Error: {str(e)}", "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
return body
|
||||||
BIN
plugins/actions/deep-dive/deep_dive_cn.png
Normal file
BIN
plugins/actions/deep-dive/deep_dive_cn.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 997 KiB |
876
plugins/actions/deep-dive/deep_dive_cn.py
Normal file
876
plugins/actions/deep-dive/deep_dive_cn.py
Normal file
@@ -0,0 +1,876 @@
|
|||||||
|
"""
|
||||||
|
title: 精读
|
||||||
|
author: Fu-Jie
|
||||||
|
author_url: https://github.com/Fu-Jie
|
||||||
|
funding_url: https://github.com/Fu-Jie/awesome-openwebui
|
||||||
|
version: 1.0.0
|
||||||
|
icon_url: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9ImN1cnJlbnRDb2xvciIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik0xMiA3djE0Ii8+PHBhdGggZD0iTTMgMThhMSAxIDAgMCAxLTEtMVY0YTEgMSAwIDAgMSAxLTFoNWE0IDQgMCAwIDEgNCA0IDQgNCAwIDAgMSA0LTRoNWExIDEgMCAwIDEgMSAxdjEzYTEgMSAwIDAgMS0xIDFoLTZhMyAzIDAgMCAwLTMgMyAzIDMgMCAwIDAtMy0zeiIvPjxwYXRoIGQ9Ik02IDEyaDIiLz48cGF0aCBkPSJNMTYgMTJoMiIvPjwvc3ZnPg==
|
||||||
|
requirements: markdown
|
||||||
|
description: 全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Standard library imports
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any, Callable, Awaitable
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Third-party imports
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from fastapi import Request
|
||||||
|
import markdown
|
||||||
|
|
||||||
|
# OpenWebUI imports
|
||||||
|
from open_webui.utils.chat import generate_chat_completion
|
||||||
|
from open_webui.models.users import Users
|
||||||
|
|
||||||
|
# Logging setup
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# HTML 模板 - 过程导向设计,支持主题自适应
|
||||||
|
# =================================================================
|
||||||
|
HTML_WRAPPER_TEMPLATE = """
|
||||||
|
<!-- OPENWEBUI_PLUGIN_OUTPUT -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{user_language}">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--dd-bg-primary: #ffffff;
|
||||||
|
--dd-bg-secondary: #f8fafc;
|
||||||
|
--dd-bg-tertiary: #f1f5f9;
|
||||||
|
--dd-text-primary: #0f172a;
|
||||||
|
--dd-text-secondary: #334155;
|
||||||
|
--dd-text-dim: #64748b;
|
||||||
|
--dd-border: #e2e8f0;
|
||||||
|
--dd-accent: #3b82f6;
|
||||||
|
--dd-accent-soft: #eff6ff;
|
||||||
|
--dd-header-gradient: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||||
|
--dd-shadow: 0 10px 40px rgba(0,0,0,0.06);
|
||||||
|
--dd-code-bg: #f1f5f9;
|
||||||
|
}
|
||||||
|
.theme-dark {
|
||||||
|
--dd-bg-primary: #1e293b;
|
||||||
|
--dd-bg-secondary: #0f172a;
|
||||||
|
--dd-bg-tertiary: #334155;
|
||||||
|
--dd-text-primary: #f1f5f9;
|
||||||
|
--dd-text-secondary: #e2e8f0;
|
||||||
|
--dd-text-dim: #94a3b8;
|
||||||
|
--dd-border: #475569;
|
||||||
|
--dd-accent: #60a5fa;
|
||||||
|
--dd-accent-soft: rgba(59, 130, 246, 0.15);
|
||||||
|
--dd-header-gradient: linear-gradient(135deg, #0f172a 0%, #1e1e2e 100%);
|
||||||
|
--dd-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||||||
|
--dd-code-bg: #334155;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
#main-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.plugin-item {
|
||||||
|
background: var(--dd-bg-primary);
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: var(--dd-shadow);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--dd-border);
|
||||||
|
}
|
||||||
|
/* STYLES_INSERTION_POINT */
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main-container">
|
||||||
|
<!-- CONTENT_INSERTION_POINT -->
|
||||||
|
</div>
|
||||||
|
<!-- SCRIPTS_INSERTION_POINT -->
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const parseColorLuma = (colorStr) => {
|
||||||
|
if (!colorStr) return null;
|
||||||
|
let m = colorStr.match(/^#?([0-9a-f]{6})$/i);
|
||||||
|
if (m) {
|
||||||
|
const hex = m[1];
|
||||||
|
const r = parseInt(hex.slice(0, 2), 16);
|
||||||
|
const g = parseInt(hex.slice(2, 4), 16);
|
||||||
|
const b = parseInt(hex.slice(4, 6), 16);
|
||||||
|
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
||||||
|
}
|
||||||
|
m = colorStr.match(/rgba?\\s*\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)/i);
|
||||||
|
if (m) {
|
||||||
|
const r = parseInt(m[1], 10);
|
||||||
|
const g = parseInt(m[2], 10);
|
||||||
|
const b = parseInt(m[3], 10);
|
||||||
|
return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const getThemeFromMeta = (doc) => {
|
||||||
|
const metas = Array.from((doc || document).querySelectorAll('meta[name="theme-color"]'));
|
||||||
|
if (!metas.length) return null;
|
||||||
|
const color = metas[metas.length - 1].content.trim();
|
||||||
|
const luma = parseColorLuma(color);
|
||||||
|
if (luma === null) return null;
|
||||||
|
return luma < 0.5 ? 'dark' : 'light';
|
||||||
|
};
|
||||||
|
const getParentDocumentSafe = () => {
|
||||||
|
try {
|
||||||
|
if (!window.parent || window.parent === window) return null;
|
||||||
|
const pDoc = window.parent.document;
|
||||||
|
void pDoc.title;
|
||||||
|
return pDoc;
|
||||||
|
} catch (err) { return null; }
|
||||||
|
};
|
||||||
|
const getThemeFromParentClass = () => {
|
||||||
|
try {
|
||||||
|
if (!window.parent || window.parent === window) return null;
|
||||||
|
const pDoc = window.parent.document;
|
||||||
|
const html = pDoc.documentElement;
|
||||||
|
const body = pDoc.body;
|
||||||
|
const htmlClass = html ? html.className : '';
|
||||||
|
const bodyClass = body ? body.className : '';
|
||||||
|
const htmlDataTheme = html ? html.getAttribute('data-theme') : '';
|
||||||
|
if (htmlDataTheme === 'dark' || bodyClass.includes('dark') || htmlClass.includes('dark')) return 'dark';
|
||||||
|
if (htmlDataTheme === 'light' || bodyClass.includes('light') || htmlClass.includes('light')) return 'light';
|
||||||
|
return null;
|
||||||
|
} catch (err) { return null; }
|
||||||
|
};
|
||||||
|
const setTheme = () => {
|
||||||
|
const parentDoc = getParentDocumentSafe();
|
||||||
|
const metaTheme = parentDoc ? getThemeFromMeta(parentDoc) : null;
|
||||||
|
const parentClassTheme = getThemeFromParentClass();
|
||||||
|
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
const chosen = metaTheme || parentClassTheme || (prefersDark ? 'dark' : 'light');
|
||||||
|
document.documentElement.classList.toggle('theme-dark', chosen === 'dark');
|
||||||
|
};
|
||||||
|
setTheme();
|
||||||
|
if (window.matchMedia) {
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', setTheme);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# LLM 提示词 - 深度下潜思维链
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = """
|
||||||
|
你是一位“深度下潜 (Deep Dive)”分析专家。你的目标是引导用户完成一个全面的思维过程,从表面理解深入到战略行动。
|
||||||
|
|
||||||
|
## 思维结构 (严格遵守)
|
||||||
|
|
||||||
|
你必须从以下四个维度剖析输入内容:
|
||||||
|
|
||||||
|
### 1. 🔍 The Context (全景)
|
||||||
|
提供一个高层级的全景视图。内容是关于什么的?核心情境、背景或正在解决的问题是什么?(2-3 段话)
|
||||||
|
|
||||||
|
### 2. 🧠 The Logic (脉络)
|
||||||
|
解构底层结构。论点是如何构建的?其中的推理逻辑、隐藏假设或起作用的思维模型是什么?(列表形式)
|
||||||
|
|
||||||
|
### 3. 💎 The Insight (洞察)
|
||||||
|
提取非显性的价值。有哪些“原来如此”的时刻?揭示了哪些深层含义、盲点或独特视角?(列表形式)
|
||||||
|
|
||||||
|
### 4. 🚀 The Path (路径)
|
||||||
|
定义战略方向。具体的、按优先级排列的下一步行动是什么?如何立即应用这些知识?(可执行步骤)
|
||||||
|
|
||||||
|
## 规则
|
||||||
|
- 使用用户指定的语言输出。
|
||||||
|
- 保持专业、分析性且富有启发性的语调。
|
||||||
|
- 聚焦于“理解的过程”,而不仅仅是结果。
|
||||||
|
- 不要包含寒暄或元对话。
|
||||||
|
"""
|
||||||
|
|
||||||
|
USER_PROMPT = """
|
||||||
|
对以下内容发起“深度下潜”:
|
||||||
|
|
||||||
|
**用户上下文:**
|
||||||
|
- 用户:{user_name}
|
||||||
|
- 时间:{current_date_time_str}
|
||||||
|
- 语言:{user_language}
|
||||||
|
|
||||||
|
**待分析内容:**
|
||||||
|
```
|
||||||
|
{long_text_content}
|
||||||
|
```
|
||||||
|
|
||||||
|
请执行完整的思维链:全景 (Context) → 脉络 (Logic) → 洞察 (Insight) → 路径 (Path)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =================================================================
|
||||||
|
# 现代 CSS 设计 - 深度下潜主题
|
||||||
|
# =================================================================
|
||||||
|
|
||||||
|
CSS_TEMPLATE = """
|
||||||
|
.deep-dive {
|
||||||
|
font-family: 'Inter', -apple-system, system-ui, sans-serif;
|
||||||
|
color: var(--dd-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-header {
|
||||||
|
background: var(--dd-header-gradient);
|
||||||
|
padding: 40px 32px;
|
||||||
|
color: white;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-header-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
border-radius: 100px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-body {
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 40px;
|
||||||
|
position: relative;
|
||||||
|
background: var(--dd-bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 思维导火索 */
|
||||||
|
.dd-body::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 52px;
|
||||||
|
top: 40px;
|
||||||
|
bottom: 40px;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--dd-border);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-step {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-step-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: var(--dd-bg-primary);
|
||||||
|
border: 2px solid var(--dd-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-step:hover .dd-step-icon {
|
||||||
|
border-color: var(--dd-accent);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-step-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-step-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--dd-accent);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-step-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--dd-text-primary);
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-text {
|
||||||
|
line-height: 1.7;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-text p { margin-bottom: 16px; }
|
||||||
|
.dd-text p:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
.dd-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-list-item {
|
||||||
|
background: var(--dd-bg-secondary);
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border-left: 4px solid var(--dd-border);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-list-item:hover {
|
||||||
|
background: var(--dd-bg-tertiary);
|
||||||
|
border-left-color: var(--dd-accent);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-list-item strong {
|
||||||
|
color: var(--dd-text-primary);
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-path-item {
|
||||||
|
background: var(--dd-accent-soft);
|
||||||
|
border-left-color: var(--dd-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-footer {
|
||||||
|
padding: 24px 32px;
|
||||||
|
background: var(--dd-bg-secondary);
|
||||||
|
border-top: 1px solid var(--dd-border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--dd-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-tag {
|
||||||
|
padding: 2px 8px;
|
||||||
|
background: var(--dd-bg-tertiary);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-text code,
|
||||||
|
.dd-list-item code {
|
||||||
|
background: var(--dd-code-bg);
|
||||||
|
color: var(--dd-text-primary);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-list-item em {
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--dd-text-dim);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
CONTENT_TEMPLATE = """
|
||||||
|
<div class="deep-dive">
|
||||||
|
<div class="dd-header">
|
||||||
|
<div class="dd-header-badge">思维过程</div>
|
||||||
|
<h1 class="dd-title">精读分析报告</h1>
|
||||||
|
<div class="dd-meta">
|
||||||
|
<span>👤 {user_name}</span>
|
||||||
|
<span>📅 {current_date_time_str}</span>
|
||||||
|
<span>📊 {word_count} 字</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dd-body">
|
||||||
|
<!-- 第一步:全景 -->
|
||||||
|
<div class="dd-step">
|
||||||
|
<div class="dd-step-icon">🔍</div>
|
||||||
|
<div class="dd-step-content">
|
||||||
|
<div class="dd-step-label">Phase 01</div>
|
||||||
|
<h2 class="dd-step-title">全景 (The Context)</h2>
|
||||||
|
<div class="dd-text">{context_html}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第二步:脉络 -->
|
||||||
|
<div class="dd-step">
|
||||||
|
<div class="dd-step-icon">🧠</div>
|
||||||
|
<div class="dd-step-content">
|
||||||
|
<div class="dd-step-label">Phase 02</div>
|
||||||
|
<h2 class="dd-step-title">脉络 (The Logic)</h2>
|
||||||
|
<div class="dd-text">{logic_html}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第三步:洞察 -->
|
||||||
|
<div class="dd-step">
|
||||||
|
<div class="dd-step-icon">💎</div>
|
||||||
|
<div class="dd-step-content">
|
||||||
|
<div class="dd-step-label">Phase 03</div>
|
||||||
|
<h2 class="dd-step-title">洞察 (The Insight)</h2>
|
||||||
|
<div class="dd-text">{insight_html}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 第四步:路径 -->
|
||||||
|
<div class="dd-step">
|
||||||
|
<div class="dd-step-icon">🚀</div>
|
||||||
|
<div class="dd-step-content">
|
||||||
|
<div class="dd-step-label">Phase 04</div>
|
||||||
|
<h2 class="dd-step-title">路径 (The Path)</h2>
|
||||||
|
<div class="dd-text">{path_html}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dd-footer">
|
||||||
|
<span>Deep Dive Engine v1.0</span>
|
||||||
|
<span><span class="dd-tag">AI 驱动分析</span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
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=200,
|
||||||
|
description="深度下潜所需的最小文本长度(字符)。",
|
||||||
|
)
|
||||||
|
CLEAR_PREVIOUS_HTML: bool = Field(
|
||||||
|
default=True,
|
||||||
|
description="是否清除之前的插件结果。",
|
||||||
|
)
|
||||||
|
MESSAGE_COUNT: int = Field(
|
||||||
|
default=1,
|
||||||
|
description="要分析的最近消息数量。",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.valves = self.Valves()
|
||||||
|
|
||||||
|
def _get_user_context(self, __user__: Optional[Dict[str, Any]]) -> Dict[str, str]:
|
||||||
|
"""安全提取用户上下文信息。"""
|
||||||
|
if isinstance(__user__, (list, tuple)):
|
||||||
|
user_data = __user__[0] if __user__ else {}
|
||||||
|
elif isinstance(__user__, dict):
|
||||||
|
user_data = __user__
|
||||||
|
else:
|
||||||
|
user_data = {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user_id": user_data.get("id", "unknown_user"),
|
||||||
|
"user_name": user_data.get("name", "用户"),
|
||||||
|
"user_language": user_data.get("language", "zh-CN"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _process_llm_output(self, llm_output: str) -> Dict[str, str]:
|
||||||
|
"""解析 LLM 输出并转换为样式化 HTML。"""
|
||||||
|
# 使用灵活的正则提取各部分
|
||||||
|
context_match = re.search(
|
||||||
|
r"###\s*1\.\s*🔍?\s*(?:全景|The Context)\s*(?:\((.*?)\))?\s*\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
logic_match = re.search(
|
||||||
|
r"###\s*2\.\s*🧠?\s*(?:脉络|The Logic)\s*(?:\((.*?)\))?\s*\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
insight_match = re.search(
|
||||||
|
r"###\s*3\.\s*💎?\s*(?:洞察|The Insight)\s*(?:\((.*?)\))?\s*\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
path_match = re.search(
|
||||||
|
r"###\s*4\.\s*🚀?\s*(?:路径|The Path)\s*(?:\((.*?)\))?\s*\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 兜底正则
|
||||||
|
if not context_match:
|
||||||
|
context_match = re.search(
|
||||||
|
r"###\s*🔍?\s*(?:全景|The Context).*?\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if not logic_match:
|
||||||
|
logic_match = re.search(
|
||||||
|
r"###\s*🧠?\s*(?:脉络|The Logic).*?\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if not insight_match:
|
||||||
|
insight_match = re.search(
|
||||||
|
r"###\s*💎?\s*(?:洞察|The Insight).*?\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
if not path_match:
|
||||||
|
path_match = re.search(
|
||||||
|
r"###\s*🚀?\s*(?:路径|The Path).*?\n(.*?)(?=\n###|$)",
|
||||||
|
llm_output,
|
||||||
|
re.DOTALL | re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
context_md = (
|
||||||
|
context_match.group(context_match.lastindex).strip()
|
||||||
|
if context_match
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
logic_md = (
|
||||||
|
logic_match.group(logic_match.lastindex).strip() if logic_match else ""
|
||||||
|
)
|
||||||
|
insight_md = (
|
||||||
|
insight_match.group(insight_match.lastindex).strip()
|
||||||
|
if insight_match
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
path_md = path_match.group(path_match.lastindex).strip() if path_match else ""
|
||||||
|
|
||||||
|
if not any([context_md, logic_md, insight_md, path_md]):
|
||||||
|
context_md = llm_output.strip()
|
||||||
|
logger.warning("LLM 输出未遵循格式,将作为全景处理。")
|
||||||
|
|
||||||
|
md_extensions = ["nl2br"]
|
||||||
|
|
||||||
|
context_html = (
|
||||||
|
markdown.markdown(context_md, extensions=md_extensions)
|
||||||
|
if context_md
|
||||||
|
else '<p class="dd-no-content">未能提取全景信息。</p>'
|
||||||
|
)
|
||||||
|
logic_html = (
|
||||||
|
self._process_list_items(logic_md, "logic")
|
||||||
|
if logic_md
|
||||||
|
else '<p class="dd-no-content">未能解构脉络。</p>'
|
||||||
|
)
|
||||||
|
insight_html = (
|
||||||
|
self._process_list_items(insight_md, "insight")
|
||||||
|
if insight_md
|
||||||
|
else '<p class="dd-no-content">未能发现洞察。</p>'
|
||||||
|
)
|
||||||
|
path_html = (
|
||||||
|
self._process_list_items(path_md, "path")
|
||||||
|
if path_md
|
||||||
|
else '<p class="dd-no-content">未能定义路径。</p>'
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"context_html": context_html,
|
||||||
|
"logic_html": logic_html,
|
||||||
|
"insight_html": insight_html,
|
||||||
|
"path_html": path_html,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _process_list_items(self, md_content: str, section_type: str) -> str:
|
||||||
|
"""将 markdown 列表转换为样式化卡片,支持完整的 markdown 格式。"""
|
||||||
|
lines = md_content.strip().split("\n")
|
||||||
|
items = []
|
||||||
|
current_paragraph = []
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
# 检查列表项(无序或有序)
|
||||||
|
bullet_match = re.match(r"^[-*]\s+(.+)$", line)
|
||||||
|
numbered_match = re.match(r"^\d+\.\s+(.+)$", line)
|
||||||
|
|
||||||
|
if bullet_match or numbered_match:
|
||||||
|
# 清空累积的段落
|
||||||
|
if current_paragraph:
|
||||||
|
para_text = " ".join(current_paragraph)
|
||||||
|
para_html = self._convert_inline_markdown(para_text)
|
||||||
|
items.append(f"<p>{para_html}</p>")
|
||||||
|
current_paragraph = []
|
||||||
|
|
||||||
|
# 提取列表项内容
|
||||||
|
text = (
|
||||||
|
bullet_match.group(1) if bullet_match else numbered_match.group(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 处理粗体标题模式:**标题:** 描述 或 **标题**: 描述
|
||||||
|
title_match = re.match(r"\*\*(.+?)\*\*[:\s:]*(.*)$", text)
|
||||||
|
if title_match:
|
||||||
|
title = self._convert_inline_markdown(title_match.group(1))
|
||||||
|
desc = self._convert_inline_markdown(title_match.group(2).strip())
|
||||||
|
path_class = "dd-path-item" if section_type == "path" else ""
|
||||||
|
item_html = f'<div class="dd-list-item {path_class}"><strong>{title}</strong>{desc}</div>'
|
||||||
|
else:
|
||||||
|
text_html = self._convert_inline_markdown(text)
|
||||||
|
path_class = "dd-path-item" if section_type == "path" else ""
|
||||||
|
item_html = (
|
||||||
|
f'<div class="dd-list-item {path_class}">{text_html}</div>'
|
||||||
|
)
|
||||||
|
items.append(item_html)
|
||||||
|
elif line and not line.startswith("#"):
|
||||||
|
# 累积段落文本
|
||||||
|
current_paragraph.append(line)
|
||||||
|
elif not line and current_paragraph:
|
||||||
|
# 空行结束段落
|
||||||
|
para_text = " ".join(current_paragraph)
|
||||||
|
para_html = self._convert_inline_markdown(para_text)
|
||||||
|
items.append(f"<p>{para_html}</p>")
|
||||||
|
current_paragraph = []
|
||||||
|
|
||||||
|
# 清空剩余段落
|
||||||
|
if current_paragraph:
|
||||||
|
para_text = " ".join(current_paragraph)
|
||||||
|
para_html = self._convert_inline_markdown(para_text)
|
||||||
|
items.append(f"<p>{para_html}</p>")
|
||||||
|
|
||||||
|
if items:
|
||||||
|
return f'<div class="dd-list">{" ".join(items)}</div>'
|
||||||
|
return f'<p class="dd-no-content">未找到条目。</p>'
|
||||||
|
|
||||||
|
def _convert_inline_markdown(self, text: str) -> str:
|
||||||
|
"""将行内 markdown(粗体、斜体、代码)转换为 HTML。"""
|
||||||
|
# 转换行内代码:`code` -> <code>code</code>
|
||||||
|
text = re.sub(r"`([^`]+)`", r"<code>\1</code>", text)
|
||||||
|
# 转换粗体:**text** -> <strong>text</strong>
|
||||||
|
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
|
||||||
|
# 转换斜体:*text* -> <em>text</em>(但不在 ** 内部)
|
||||||
|
text = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
async def _emit_status(
|
||||||
|
self,
|
||||||
|
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||||
|
description: str,
|
||||||
|
done: bool = False,
|
||||||
|
):
|
||||||
|
"""发送状态更新事件。"""
|
||||||
|
if self.valves.SHOW_STATUS and emitter:
|
||||||
|
await emitter(
|
||||||
|
{"type": "status", "data": {"description": description, "done": done}}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _emit_notification(
|
||||||
|
self,
|
||||||
|
emitter: Optional[Callable[[Any], Awaitable[None]]],
|
||||||
|
content: str,
|
||||||
|
ntype: str = "info",
|
||||||
|
):
|
||||||
|
"""发送通知事件。"""
|
||||||
|
if emitter:
|
||||||
|
await emitter(
|
||||||
|
{"type": "notification", "data": {"type": ntype, "content": content}}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _remove_existing_html(self, content: str) -> str:
|
||||||
|
"""移除已有的插件生成的 HTML。"""
|
||||||
|
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):
|
||||||
|
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: str,
|
||||||
|
new_content: str,
|
||||||
|
new_styles: str = "",
|
||||||
|
user_language: str = "zh-CN",
|
||||||
|
) -> str:
|
||||||
|
"""合并新内容到 HTML 容器。"""
|
||||||
|
if "<!-- OPENWEBUI_PLUGIN_OUTPUT -->" in existing_html:
|
||||||
|
base_html = re.sub(r"^```html\s*", "", existing_html)
|
||||||
|
base_html = re.sub(r"\s*```$", "", base_html)
|
||||||
|
else:
|
||||||
|
base_html = HTML_WRAPPER_TEMPLATE.replace("{user_language}", user_language)
|
||||||
|
|
||||||
|
wrapped = f'<div class="plugin-item">\n{new_content}\n</div>'
|
||||||
|
|
||||||
|
if new_styles:
|
||||||
|
base_html = base_html.replace(
|
||||||
|
"/* STYLES_INSERTION_POINT */",
|
||||||
|
f"{new_styles}\n/* STYLES_INSERTION_POINT */",
|
||||||
|
)
|
||||||
|
|
||||||
|
base_html = base_html.replace(
|
||||||
|
"<!-- CONTENT_INSERTION_POINT -->",
|
||||||
|
f"{wrapped}\n<!-- CONTENT_INSERTION_POINT -->",
|
||||||
|
)
|
||||||
|
|
||||||
|
return base_html.strip()
|
||||||
|
|
||||||
|
def _build_content_html(self, context: dict) -> str:
|
||||||
|
"""构建内容 HTML。"""
|
||||||
|
html = CONTENT_TEMPLATE
|
||||||
|
for key, value in context.items():
|
||||||
|
html = html.replace(f"{{{key}}}", str(value))
|
||||||
|
return html
|
||||||
|
|
||||||
|
async def action(
|
||||||
|
self,
|
||||||
|
body: dict,
|
||||||
|
__user__: Optional[Dict[str, Any]] = None,
|
||||||
|
__event_emitter__: Optional[Callable[[Any], Awaitable[None]]] = None,
|
||||||
|
__request__: Optional[Request] = None,
|
||||||
|
) -> Optional[dict]:
|
||||||
|
logger.info("Action: 精读 v1.0.0 启动")
|
||||||
|
|
||||||
|
user_ctx = self._get_user_context(__user__)
|
||||||
|
user_id = user_ctx["user_id"]
|
||||||
|
user_name = user_ctx["user_name"]
|
||||||
|
user_language = user_ctx["user_language"]
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
current_date_time_str = now.strftime("%Y年%m月%d日 %H:%M")
|
||||||
|
|
||||||
|
original_content = ""
|
||||||
|
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 = self._extract_text_content(msg.get("content"))
|
||||||
|
if text:
|
||||||
|
aggregated_parts.append(text)
|
||||||
|
|
||||||
|
if not aggregated_parts:
|
||||||
|
raise ValueError("未找到文本内容。")
|
||||||
|
|
||||||
|
original_content = "\n\n---\n\n".join(aggregated_parts)
|
||||||
|
word_count = len(original_content)
|
||||||
|
|
||||||
|
if len(original_content) < self.valves.MIN_TEXT_LENGTH:
|
||||||
|
msg = f"内容过短({len(original_content)} 字符)。精读至少需要 {self.valves.MIN_TEXT_LENGTH} 字符才能进行有意义的分析。"
|
||||||
|
await self._emit_notification(__event_emitter__, msg, "warning")
|
||||||
|
return {"messages": [{"role": "assistant", "content": f"⚠️ {msg}"}]}
|
||||||
|
|
||||||
|
await self._emit_notification(
|
||||||
|
__event_emitter__, "📖 正在发起精读分析...", "info"
|
||||||
|
)
|
||||||
|
await self._emit_status(
|
||||||
|
__event_emitter__, "📖 精读:正在分析全景与脉络...", False
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = USER_PROMPT.format(
|
||||||
|
user_name=user_name,
|
||||||
|
current_date_time_str=current_date_time_str,
|
||||||
|
user_language=user_language,
|
||||||
|
long_text_content=original_content,
|
||||||
|
)
|
||||||
|
|
||||||
|
model = self.valves.MODEL_ID or body.get("model")
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": SYSTEM_PROMPT},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
"stream": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
user_obj = Users.get_user_by_id(user_id)
|
||||||
|
if not user_obj:
|
||||||
|
raise ValueError(f"未找到用户:{user_id}")
|
||||||
|
|
||||||
|
response = await generate_chat_completion(__request__, payload, user_obj)
|
||||||
|
llm_output = response["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
processed = self._process_llm_output(llm_output)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"user_name": user_name,
|
||||||
|
"current_date_time_str": current_date_time_str,
|
||||||
|
"word_count": word_count,
|
||||||
|
**processed,
|
||||||
|
}
|
||||||
|
|
||||||
|
content_html = self._build_content_html(context)
|
||||||
|
|
||||||
|
# 处理已有 HTML
|
||||||
|
existing = ""
|
||||||
|
match = re.search(
|
||||||
|
r"```html\s*(<!-- OPENWEBUI_PLUGIN_OUTPUT -->[\s\S]*?)```",
|
||||||
|
original_content,
|
||||||
|
)
|
||||||
|
if match:
|
||||||
|
existing = match.group(1)
|
||||||
|
|
||||||
|
if self.valves.CLEAR_PREVIOUS_HTML or not existing:
|
||||||
|
original_content = self._remove_existing_html(original_content)
|
||||||
|
final_html = self._merge_html(
|
||||||
|
"", content_html, CSS_TEMPLATE, user_language
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
original_content = self._remove_existing_html(original_content)
|
||||||
|
final_html = self._merge_html(
|
||||||
|
existing, content_html, CSS_TEMPLATE, user_language
|
||||||
|
)
|
||||||
|
|
||||||
|
body["messages"][-1][
|
||||||
|
"content"
|
||||||
|
] = f"{original_content}\n\n```html\n{final_html}\n```"
|
||||||
|
|
||||||
|
await self._emit_status(__event_emitter__, "📖 精读完成!", True)
|
||||||
|
await self._emit_notification(
|
||||||
|
__event_emitter__,
|
||||||
|
f"📖 精读完成,{user_name}!思维链已生成。",
|
||||||
|
"success",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Deep Dive 错误:{e}", exc_info=True)
|
||||||
|
body["messages"][-1][
|
||||||
|
"content"
|
||||||
|
] = f"{original_content}\n\n❌ **错误:** {str(e)}"
|
||||||
|
await self._emit_status(__event_emitter__, "精读失败。", True)
|
||||||
|
await self._emit_notification(__event_emitter__, f"错误:{str(e)}", "error")
|
||||||
|
|
||||||
|
return body
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
"""
|
||||||
|
Fetch remote plugin versions from OpenWebUI Community
|
||||||
|
获取远程插件版本信息
|
||||||
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@@ -5,22 +10,17 @@ import sys
|
|||||||
# Add current directory to path
|
# Add current directory to path
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
try:
|
from openwebui_community_client import get_client
|
||||||
from openwebui_stats import OpenWebUIStats
|
|
||||||
except ImportError:
|
|
||||||
print("Error: openwebui_stats.py not found.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Try to get token from env
|
try:
|
||||||
token = os.environ.get("OPENWEBUI_API_KEY")
|
client = get_client()
|
||||||
if not token:
|
except ValueError as e:
|
||||||
print("Error: OPENWEBUI_API_KEY environment variable not set.")
|
print(f"Error: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print("Fetching remote plugins from OpenWebUI...")
|
print("Fetching remote plugins from OpenWebUI Community...")
|
||||||
client = OpenWebUIStats(token)
|
|
||||||
try:
|
try:
|
||||||
posts = client.get_all_posts()
|
posts = client.get_all_posts()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -29,9 +29,6 @@ def main():
|
|||||||
|
|
||||||
formatted_plugins = []
|
formatted_plugins = []
|
||||||
for post in posts:
|
for post in posts:
|
||||||
# Save the full raw post object to ensure we have "compliant update json data"
|
|
||||||
# We inject a 'type' field just for the comparison script to know it's remote,
|
|
||||||
# but otherwise keep the structure identical to the API response.
|
|
||||||
post["type"] = "remote_plugin"
|
post["type"] = "remote_plugin"
|
||||||
formatted_plugins.append(post)
|
formatted_plugins.append(post)
|
||||||
|
|
||||||
|
|||||||
374
scripts/openwebui_community_client.py
Normal file
374
scripts/openwebui_community_client.py
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
"""
|
||||||
|
OpenWebUI Community Client
|
||||||
|
统一封装所有与 OpenWebUI 官方社区 (openwebui.com) 的 API 交互。
|
||||||
|
|
||||||
|
功能:
|
||||||
|
- 获取用户发布的插件/帖子
|
||||||
|
- 更新插件内容和元数据
|
||||||
|
- 版本比较
|
||||||
|
- 同步插件 ID
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
from openwebui_community_client import OpenWebUICommunityClient
|
||||||
|
|
||||||
|
client = OpenWebUICommunityClient(api_key="your_api_key")
|
||||||
|
posts = client.get_all_posts()
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import requests
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from typing import Optional, Dict, List, Any, Tuple
|
||||||
|
|
||||||
|
# 北京时区 (UTC+8)
|
||||||
|
BEIJING_TZ = timezone(timedelta(hours=8))
|
||||||
|
|
||||||
|
|
||||||
|
class OpenWebUICommunityClient:
|
||||||
|
"""OpenWebUI 官方社区 API 客户端"""
|
||||||
|
|
||||||
|
BASE_URL = "https://api.openwebui.com/api/v1"
|
||||||
|
|
||||||
|
def __init__(self, api_key: str, user_id: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
初始化客户端
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key: OpenWebUI API Key (JWT Token)
|
||||||
|
user_id: 用户 ID,如果为 None 则从 token 中解析
|
||||||
|
"""
|
||||||
|
self.api_key = api_key
|
||||||
|
self.user_id = user_id or self._parse_user_id_from_token(api_key)
|
||||||
|
self.headers = {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
def _parse_user_id_from_token(self, token: str) -> Optional[str]:
|
||||||
|
"""从 JWT Token 中解析用户 ID"""
|
||||||
|
try:
|
||||||
|
parts = token.split(".")
|
||||||
|
if len(parts) >= 2:
|
||||||
|
payload = parts[1]
|
||||||
|
# 添加 padding
|
||||||
|
padding = 4 - len(payload) % 4
|
||||||
|
if padding != 4:
|
||||||
|
payload += "=" * padding
|
||||||
|
decoded = base64.urlsafe_b64decode(payload)
|
||||||
|
data = json.loads(decoded)
|
||||||
|
return data.get("id") or data.get("sub")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ========== 帖子/插件获取 ==========
|
||||||
|
|
||||||
|
def get_user_posts(self, sort: str = "new", page: int = 1) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
获取用户发布的帖子列表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sort: 排序方式 (new/top/hot)
|
||||||
|
page: 页码
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
帖子列表
|
||||||
|
"""
|
||||||
|
url = f"{self.BASE_URL}/posts/user/{self.user_id}?sort={sort}&page={page}"
|
||||||
|
response = requests.get(url, headers=self.headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def get_all_posts(self, sort: str = "new") -> List[Dict]:
|
||||||
|
"""获取所有帖子(自动分页)"""
|
||||||
|
all_posts = []
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
posts = self.get_user_posts(sort=sort, page=page)
|
||||||
|
if not posts:
|
||||||
|
break
|
||||||
|
all_posts.extend(posts)
|
||||||
|
page += 1
|
||||||
|
return all_posts
|
||||||
|
|
||||||
|
def get_post(self, post_id: str) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
获取单个帖子详情
|
||||||
|
|
||||||
|
Args:
|
||||||
|
post_id: 帖子 ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
帖子数据,如果不存在返回 None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = f"{self.BASE_URL}/posts/{post_id}"
|
||||||
|
response = requests.get(url, headers=self.headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
if e.response.status_code == 404:
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
|
||||||
|
# ========== 帖子/插件更新 ==========
|
||||||
|
|
||||||
|
def update_post(self, post_id: str, post_data: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
更新帖子
|
||||||
|
|
||||||
|
Args:
|
||||||
|
post_id: 帖子 ID
|
||||||
|
post_data: 完整的帖子数据
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功
|
||||||
|
"""
|
||||||
|
url = f"{self.BASE_URL}/posts/{post_id}/update"
|
||||||
|
response = requests.post(url, headers=self.headers, json=post_data)
|
||||||
|
response.raise_for_status()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def update_plugin(
|
||||||
|
self,
|
||||||
|
post_id: str,
|
||||||
|
source_code: str,
|
||||||
|
readme_content: Optional[str] = None,
|
||||||
|
metadata: Optional[Dict] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
更新插件(代码 + README + 元数据)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
post_id: 帖子 ID
|
||||||
|
source_code: 插件源代码
|
||||||
|
readme_content: README 内容(用于社区页面展示)
|
||||||
|
metadata: 插件元数据(title, version, description 等)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功
|
||||||
|
"""
|
||||||
|
post_data = self.get_post(post_id)
|
||||||
|
if not post_data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 确保结构存在
|
||||||
|
if "data" not in post_data:
|
||||||
|
post_data["data"] = {}
|
||||||
|
if "function" not in post_data["data"]:
|
||||||
|
post_data["data"]["function"] = {}
|
||||||
|
if "meta" not in post_data["data"]["function"]:
|
||||||
|
post_data["data"]["function"]["meta"] = {}
|
||||||
|
if "manifest" not in post_data["data"]["function"]["meta"]:
|
||||||
|
post_data["data"]["function"]["meta"]["manifest"] = {}
|
||||||
|
|
||||||
|
# 更新源代码
|
||||||
|
post_data["data"]["function"]["content"] = source_code
|
||||||
|
|
||||||
|
# 更新 README(社区页面展示内容)
|
||||||
|
if readme_content:
|
||||||
|
post_data["content"] = readme_content
|
||||||
|
|
||||||
|
# 更新元数据
|
||||||
|
if metadata:
|
||||||
|
post_data["data"]["function"]["meta"]["manifest"].update(metadata)
|
||||||
|
if "title" in metadata:
|
||||||
|
post_data["title"] = metadata["title"]
|
||||||
|
post_data["data"]["function"]["name"] = metadata["title"]
|
||||||
|
if "description" in metadata:
|
||||||
|
post_data["data"]["function"]["meta"]["description"] = metadata[
|
||||||
|
"description"
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.update_post(post_id, post_data)
|
||||||
|
|
||||||
|
# ========== 版本比较 ==========
|
||||||
|
|
||||||
|
def get_remote_version(self, post_id: str) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
获取远程插件版本
|
||||||
|
|
||||||
|
Args:
|
||||||
|
post_id: 帖子 ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
版本号,如果不存在返回 None
|
||||||
|
"""
|
||||||
|
post_data = self.get_post(post_id)
|
||||||
|
if not post_data:
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
post_data.get("data", {})
|
||||||
|
.get("function", {})
|
||||||
|
.get("meta", {})
|
||||||
|
.get("manifest", {})
|
||||||
|
.get("version")
|
||||||
|
)
|
||||||
|
|
||||||
|
def version_needs_update(self, post_id: str, local_version: str) -> bool:
|
||||||
|
"""
|
||||||
|
检查是否需要更新
|
||||||
|
|
||||||
|
Args:
|
||||||
|
post_id: 帖子 ID
|
||||||
|
local_version: 本地版本号
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
如果本地版本与远程不同,返回 True
|
||||||
|
"""
|
||||||
|
remote_version = self.get_remote_version(post_id)
|
||||||
|
if not remote_version:
|
||||||
|
return True # 远程不存在,需要更新
|
||||||
|
return local_version != remote_version
|
||||||
|
|
||||||
|
# ========== 插件发布 ==========
|
||||||
|
|
||||||
|
def publish_plugin_from_file(
|
||||||
|
self, file_path: str, force: bool = False
|
||||||
|
) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
从文件发布插件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: 插件文件路径
|
||||||
|
force: 是否强制更新(忽略版本检查)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(是否成功, 消息)
|
||||||
|
"""
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
metadata = self._parse_frontmatter(content)
|
||||||
|
if not metadata:
|
||||||
|
return False, "No frontmatter found"
|
||||||
|
|
||||||
|
post_id = metadata.get("openwebui_id") or metadata.get("post_id")
|
||||||
|
if not post_id:
|
||||||
|
return False, "No openwebui_id found"
|
||||||
|
|
||||||
|
local_version = metadata.get("version")
|
||||||
|
|
||||||
|
# 版本检查
|
||||||
|
if not force and local_version:
|
||||||
|
if not self.version_needs_update(post_id, local_version):
|
||||||
|
return True, f"Skipped: version {local_version} matches remote"
|
||||||
|
|
||||||
|
# 查找 README
|
||||||
|
readme_content = self._find_readme(file_path)
|
||||||
|
|
||||||
|
# 更新
|
||||||
|
success = self.update_plugin(
|
||||||
|
post_id=post_id,
|
||||||
|
source_code=content,
|
||||||
|
readme_content=readme_content or metadata.get("description", ""),
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return True, f"Updated to version {local_version}"
|
||||||
|
return False, "Update failed"
|
||||||
|
|
||||||
|
def _parse_frontmatter(self, content: str) -> Dict[str, str]:
|
||||||
|
"""解析插件文件的 frontmatter"""
|
||||||
|
match = re.search(r'^\s*"""\n(.*?)\n"""', content, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
match = re.search(r'"""\n(.*?)\n"""', content, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
frontmatter = match.group(1)
|
||||||
|
meta = {}
|
||||||
|
for line in frontmatter.split("\n"):
|
||||||
|
if ":" in line:
|
||||||
|
key, value = line.split(":", 1)
|
||||||
|
meta[key.strip()] = value.strip()
|
||||||
|
return meta
|
||||||
|
|
||||||
|
def _find_readme(self, plugin_file_path: str) -> Optional[str]:
|
||||||
|
"""查找插件对应的 README 文件"""
|
||||||
|
plugin_dir = os.path.dirname(plugin_file_path)
|
||||||
|
base_name = os.path.basename(plugin_file_path).lower()
|
||||||
|
|
||||||
|
# 确定优先顺序
|
||||||
|
if base_name.endswith("_cn.py"):
|
||||||
|
readme_files = ["README_CN.md", "README.md"]
|
||||||
|
else:
|
||||||
|
readme_files = ["README.md", "README_CN.md"]
|
||||||
|
|
||||||
|
for readme_name in readme_files:
|
||||||
|
readme_path = os.path.join(plugin_dir, readme_name)
|
||||||
|
if os.path.exists(readme_path):
|
||||||
|
with open(readme_path, "r", encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ========== 统计功能 ==========
|
||||||
|
|
||||||
|
def generate_stats(self, posts: List[Dict]) -> Dict:
|
||||||
|
"""
|
||||||
|
生成统计数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
posts: 帖子列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
统计数据字典
|
||||||
|
"""
|
||||||
|
stats = {
|
||||||
|
"total_posts": len(posts),
|
||||||
|
"total_downloads": 0,
|
||||||
|
"total_likes": 0,
|
||||||
|
"posts_by_type": {},
|
||||||
|
"posts_detail": [],
|
||||||
|
"generated_at": datetime.now(BEIJING_TZ).isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for post in posts:
|
||||||
|
downloads = post.get("downloadCount", 0)
|
||||||
|
likes = post.get("likeCount", 0)
|
||||||
|
post_type = post.get("type", "unknown")
|
||||||
|
|
||||||
|
stats["total_downloads"] += downloads
|
||||||
|
stats["total_likes"] += likes
|
||||||
|
stats["posts_by_type"][post_type] = (
|
||||||
|
stats["posts_by_type"].get(post_type, 0) + 1
|
||||||
|
)
|
||||||
|
|
||||||
|
stats["posts_detail"].append(
|
||||||
|
{
|
||||||
|
"id": post.get("id"),
|
||||||
|
"title": post.get("title"),
|
||||||
|
"type": post_type,
|
||||||
|
"downloads": downloads,
|
||||||
|
"likes": likes,
|
||||||
|
"created_at": post.get("createdAt"),
|
||||||
|
"updated_at": post.get("updatedAt"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 按下载量排序
|
||||||
|
stats["posts_detail"].sort(key=lambda x: x["downloads"], reverse=True)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
# 便捷函数
|
||||||
|
def get_client(api_key: Optional[str] = None) -> OpenWebUICommunityClient:
|
||||||
|
"""
|
||||||
|
获取客户端实例
|
||||||
|
|
||||||
|
Args:
|
||||||
|
api_key: API Key,如果为 None 则从环境变量获取
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
OpenWebUICommunityClient 实例
|
||||||
|
"""
|
||||||
|
key = api_key or os.environ.get("OPENWEBUI_API_KEY")
|
||||||
|
if not key:
|
||||||
|
raise ValueError("OPENWEBUI_API_KEY not set")
|
||||||
|
return OpenWebUICommunityClient(key)
|
||||||
@@ -1,261 +1,88 @@
|
|||||||
|
"""
|
||||||
|
Publish plugins to OpenWebUI Community
|
||||||
|
使用 OpenWebUICommunityClient 发布插件到官方社区
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python scripts/publish_plugin.py # 只更新有版本变化的插件
|
||||||
|
python scripts/publish_plugin.py --force # 强制更新所有插件
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
import re
|
import re
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
# Add current directory to path
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from openwebui_community_client import OpenWebUICommunityClient, get_client
|
||||||
|
|
||||||
|
|
||||||
def parse_frontmatter(content):
|
def find_plugins_with_id(plugins_dir: str) -> list:
|
||||||
"""Extracts metadata from the python file docstring."""
|
"""查找所有带 openwebui_id 的插件文件"""
|
||||||
# Allow leading whitespace and handle potential shebangs
|
plugins = []
|
||||||
match = re.search(r'^\s*"""\n(.*?)\n"""', content, re.DOTALL)
|
for root, _, files in os.walk(plugins_dir):
|
||||||
if not match:
|
for file in files:
|
||||||
# Fallback for files starting with comments or shebangs
|
if file.endswith(".py"):
|
||||||
match = re.search(r'"""\n(.*?)\n"""', content, re.DOTALL)
|
file_path = os.path.join(root, file)
|
||||||
if not match:
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
return {}
|
content = f.read(2000) # 只读前 2000 字符检查 ID
|
||||||
|
|
||||||
frontmatter = match.group(1)
|
id_match = re.search(
|
||||||
meta = {}
|
r"(?:openwebui_id|post_id):\s*([a-z0-9-]+)", content
|
||||||
for line in frontmatter.split("\n"):
|
)
|
||||||
if ":" in line:
|
if id_match:
|
||||||
key, value = line.split(":", 1)
|
plugins.append(
|
||||||
meta[key.strip()] = value.strip()
|
{"file_path": file_path, "post_id": id_match.group(1).strip()}
|
||||||
return meta
|
)
|
||||||
|
return plugins
|
||||||
|
|
||||||
def sync_frontmatter(file_path, content, meta, post_data):
|
|
||||||
"""Syncs remote metadata back to local file frontmatter."""
|
|
||||||
changed = False
|
|
||||||
new_meta = meta.copy()
|
|
||||||
|
|
||||||
# 1. Sync ID
|
|
||||||
if "openwebui_id" not in new_meta and "post_id" not in new_meta:
|
|
||||||
new_meta["openwebui_id"] = post_data.get("id")
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# 2. Sync Icon URL (often set in UI)
|
|
||||||
manifest = (
|
|
||||||
post_data.get("data", {})
|
|
||||||
.get("function", {})
|
|
||||||
.get("meta", {})
|
|
||||||
.get("manifest", {})
|
|
||||||
)
|
|
||||||
if "icon_url" not in new_meta and manifest.get("icon_url"):
|
|
||||||
new_meta["icon_url"] = manifest.get("icon_url")
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# 3. Sync other fields if missing locally
|
|
||||||
for field in ["author", "author_url", "funding_url"]:
|
|
||||||
if field not in new_meta and manifest.get(field):
|
|
||||||
new_meta[field] = manifest.get(field)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
if changed:
|
|
||||||
print(f" Syncing metadata back to {os.path.basename(file_path)}...")
|
|
||||||
# Reconstruct frontmatter
|
|
||||||
# We need to replace the content inside the first """ ... """
|
|
||||||
# This is a bit fragile with regex but sufficient for standard files
|
|
||||||
|
|
||||||
def replacement(match):
|
|
||||||
lines = []
|
|
||||||
# Keep existing description or comments if we can't parse them easily?
|
|
||||||
# Actually, let's just reconstruct the key-values we know
|
|
||||||
# and try to preserve the description if it was at the end
|
|
||||||
|
|
||||||
# Simple approach: Rebuild the whole block based on new_meta
|
|
||||||
# This might lose comments inside the frontmatter, but standard format is simple keys
|
|
||||||
|
|
||||||
# Try to preserve order: title, author, ..., version, ..., description
|
|
||||||
ordered_keys = [
|
|
||||||
"title",
|
|
||||||
"author",
|
|
||||||
"author_url",
|
|
||||||
"funding_url",
|
|
||||||
"version",
|
|
||||||
"openwebui_id",
|
|
||||||
"icon_url",
|
|
||||||
"requirements",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
|
|
||||||
block = ['"""']
|
|
||||||
|
|
||||||
# Add known keys in order
|
|
||||||
for k in ordered_keys:
|
|
||||||
if k in new_meta:
|
|
||||||
block.append(f"{k}: {new_meta[k]}")
|
|
||||||
|
|
||||||
# Add any other custom keys
|
|
||||||
for k, v in new_meta.items():
|
|
||||||
if k not in ordered_keys:
|
|
||||||
block.append(f"{k}: {v}")
|
|
||||||
|
|
||||||
block.append('"""')
|
|
||||||
return "\n".join(block)
|
|
||||||
|
|
||||||
new_content = re.sub(
|
|
||||||
r'^"""\n(.*?)\n"""', replacement, content, count=1, flags=re.DOTALL
|
|
||||||
)
|
|
||||||
|
|
||||||
# If regex didn't match (e.g. leading whitespace), try with whitespace
|
|
||||||
if new_content == content:
|
|
||||||
new_content = re.sub(
|
|
||||||
r'^\s*"""\n(.*?)\n"""', replacement, content, count=1, flags=re.DOTALL
|
|
||||||
)
|
|
||||||
|
|
||||||
if new_content != content:
|
|
||||||
with open(file_path, "w", encoding="utf-8") as f:
|
|
||||||
f.write(new_content)
|
|
||||||
return new_content # Return updated content
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
|
|
||||||
def update_plugin(file_path, post_id, token):
|
|
||||||
print(f"Processing {os.path.basename(file_path)} (ID: {post_id})...")
|
|
||||||
|
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
meta = parse_frontmatter(content)
|
|
||||||
if not meta:
|
|
||||||
print(f" Skipping: No frontmatter found.")
|
|
||||||
return False
|
|
||||||
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Accept": "application/json",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 1. Fetch existing post
|
|
||||||
try:
|
|
||||||
response = requests.get(
|
|
||||||
f"https://api.openwebui.com/api/v1/posts/{post_id}", headers=headers
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
post_data = response.json()
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Error fetching post: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 1.5 Sync Metadata back to local file
|
|
||||||
try:
|
|
||||||
content = sync_frontmatter(file_path, content, meta, post_data)
|
|
||||||
# Re-parse meta in case it changed
|
|
||||||
meta = parse_frontmatter(content)
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Warning: Failed to sync local metadata: {e}")
|
|
||||||
|
|
||||||
# 2. Update ONLY Content and Manifest
|
|
||||||
try:
|
|
||||||
# Ensure structure exists before populating nested fields
|
|
||||||
if "data" not in post_data:
|
|
||||||
post_data["data"] = {}
|
|
||||||
if "function" not in post_data["data"]:
|
|
||||||
post_data["data"]["function"] = {}
|
|
||||||
if "meta" not in post_data["data"]["function"]:
|
|
||||||
post_data["data"]["function"]["meta"] = {}
|
|
||||||
if "manifest" not in post_data["data"]["function"]["meta"]:
|
|
||||||
post_data["data"]["function"]["meta"]["manifest"] = {}
|
|
||||||
|
|
||||||
# Update 1: The Source Code (Inner Content)
|
|
||||||
post_data["data"]["function"]["content"] = content
|
|
||||||
|
|
||||||
# Update 2: The Post Body/README (Outer Content)
|
|
||||||
# Try to find a matching README file
|
|
||||||
plugin_dir = os.path.dirname(file_path)
|
|
||||||
base_name = os.path.basename(file_path).lower()
|
|
||||||
readme_content = None
|
|
||||||
|
|
||||||
# Determine preferred README filename
|
|
||||||
readme_files = []
|
|
||||||
if base_name.endswith("_cn.py"):
|
|
||||||
readme_files = ["README_CN.md", "README.md"]
|
|
||||||
else:
|
|
||||||
readme_files = ["README.md", "README_CN.md"]
|
|
||||||
|
|
||||||
for readme_name in readme_files:
|
|
||||||
readme_path = os.path.join(plugin_dir, readme_name)
|
|
||||||
if os.path.exists(readme_path):
|
|
||||||
try:
|
|
||||||
with open(readme_path, "r", encoding="utf-8") as f:
|
|
||||||
readme_content = f.read()
|
|
||||||
print(f" Using README: {readme_name}")
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Error reading {readme_name}: {e}")
|
|
||||||
|
|
||||||
if readme_content:
|
|
||||||
post_data["content"] = readme_content
|
|
||||||
elif "description" in meta:
|
|
||||||
post_data["content"] = meta["description"]
|
|
||||||
else:
|
|
||||||
post_data["content"] = ""
|
|
||||||
|
|
||||||
# Update Manifest (Metadata)
|
|
||||||
post_data["data"]["function"]["meta"]["manifest"].update(meta)
|
|
||||||
|
|
||||||
# Sync top-level fields for consistency
|
|
||||||
if "title" in meta:
|
|
||||||
post_data["title"] = meta["title"]
|
|
||||||
post_data["data"]["function"]["name"] = meta["title"]
|
|
||||||
if "description" in meta:
|
|
||||||
post_data["data"]["function"]["meta"]["description"] = meta["description"]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Error preparing update: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 3. Submit Update
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"https://api.openwebui.com/api/v1/posts/{post_id}/update",
|
|
||||||
headers=headers,
|
|
||||||
json=post_data,
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
print(f" ✅ Success!")
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ❌ Failed: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
token = os.environ.get("OPENWEBUI_API_KEY")
|
parser = argparse.ArgumentParser(description="Publish plugins to OpenWebUI Market")
|
||||||
if not token:
|
parser.add_argument(
|
||||||
print("Error: OPENWEBUI_API_KEY not set.")
|
"--force", action="store_true", help="Force update even if version matches"
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = get_client()
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
plugins_dir = os.path.join(base_dir, "plugins")
|
plugins_dir = os.path.join(base_dir, "plugins")
|
||||||
|
|
||||||
count = 0
|
plugins = find_plugins_with_id(plugins_dir)
|
||||||
# Walk through plugins directory
|
print(f"Found {len(plugins)} plugins with OpenWebUI ID.\n")
|
||||||
for root, _, files in os.walk(plugins_dir):
|
|
||||||
for file in files:
|
|
||||||
if file.endswith(".py"):
|
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
|
|
||||||
# Check for ID in file content without full parse first
|
updated = 0
|
||||||
with open(file_path, "r", encoding="utf-8") as f:
|
skipped = 0
|
||||||
content = f.read(
|
failed = 0
|
||||||
2000
|
|
||||||
) # Read first 2000 chars is enough for frontmatter
|
|
||||||
|
|
||||||
# Simple regex to find ID
|
for plugin in plugins:
|
||||||
id_match = re.search(
|
file_path = plugin["file_path"]
|
||||||
r"(?:openwebui_id|post_id):\s*([a-z0-9-]+)", content
|
file_name = os.path.basename(file_path)
|
||||||
)
|
post_id = plugin["post_id"]
|
||||||
|
|
||||||
if id_match:
|
print(f"Processing {file_name} (ID: {post_id})...")
|
||||||
post_id = id_match.group(1).strip()
|
|
||||||
update_plugin(file_path, post_id, token)
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
print(f"\nFinished. Updated {count} plugins.")
|
success, message = client.publish_plugin_from_file(file_path, force=args.force)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
if "Skipped" in message:
|
||||||
|
print(f" ⏭️ {message}")
|
||||||
|
skipped += 1
|
||||||
|
else:
|
||||||
|
print(f" ✅ {message}")
|
||||||
|
updated += 1
|
||||||
|
else:
|
||||||
|
print(f" ❌ {message}")
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"Finished: {updated} updated, {skipped} skipped, {failed} failed")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
"""
|
||||||
|
Sync OpenWebUI Post IDs to local plugin files
|
||||||
|
同步远程插件 ID 到本地文件
|
||||||
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
@@ -6,11 +11,12 @@ import difflib
|
|||||||
# Add current directory to path
|
# Add current directory to path
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from openwebui_community_client import get_client
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from openwebui_stats import OpenWebUIStats
|
|
||||||
from extract_plugin_versions import scan_plugins_directory
|
from extract_plugin_versions import scan_plugins_directory
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("Error: Helper scripts not found.")
|
print("Error: extract_plugin_versions.py not found.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -60,13 +66,13 @@ def insert_id_into_file(file_path, post_id):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
token = os.environ.get("OPENWEBUI_API_KEY")
|
try:
|
||||||
if not token:
|
client = get_client()
|
||||||
print("Error: OPENWEBUI_API_KEY environment variable not set.")
|
except ValueError as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print("Fetching remote posts...")
|
print("Fetching remote posts from OpenWebUI Community...")
|
||||||
client = OpenWebUIStats(token)
|
|
||||||
remote_posts = client.get_all_posts()
|
remote_posts = client.get_all_posts()
|
||||||
print(f"Fetched {len(remote_posts)} remote posts.")
|
print(f"Fetched {len(remote_posts)} remote posts.")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user