diff --git a/.github/workflows/publish_new_plugin.yml b/.github/workflows/publish_new_plugin.yml new file mode 100644 index 0000000..2de2fb1 --- /dev/null +++ b/.github/workflows/publish_new_plugin.yml @@ -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 diff --git a/docs/plugins/actions/deep-dive.md b/docs/plugins/actions/deep-dive.md new file mode 100644 index 0000000..bb3979a --- /dev/null +++ b/docs/plugins/actions/deep-dive.md @@ -0,0 +1,111 @@ +# Deep Dive + +Action +v1.0.0 + +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 `` 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 } diff --git a/docs/plugins/actions/deep-dive.zh.md b/docs/plugins/actions/deep-dive.zh.md new file mode 100644 index 0000000..d577a3c --- /dev/null +++ b/docs/plugins/actions/deep-dive.zh.md @@ -0,0 +1,111 @@ +# 精读 (Deep Dive) + +Action +v1.0.0 + +全方位的思维透镜 —— 从背景全景到逻辑脉络,从深度洞察到行动路径。 + +--- + +## 概述 + +精读插件改变了您理解复杂内容的方式,通过结构化的思维过程引导您进行深度分析。它不仅仅是摘要,而是从四个阶段解构内容: + +- **🔍 全景 (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 的深色/浅色主题: + +- 从父文档 `` 标签检测主题 +- 回退到 `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 } diff --git a/docs/plugins/actions/index.md b/docs/plugins/actions/index.md index 31208f4..133d687 100644 --- a/docs/plugins/actions/index.md +++ b/docs/plugins/actions/index.md @@ -67,15 +67,15 @@ Actions are interactive plugins that: [: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** diff --git a/docs/plugins/actions/index.zh.md b/docs/plugins/actions/index.zh.md index 3d82ca5..810f5be 100644 --- a/docs/plugins/actions/index.zh.md +++ b/docs/plugins/actions/index.zh.md @@ -67,15 +67,15 @@ Actions 是交互式插件,能够: [: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** diff --git a/plugins/actions/deep-dive/README.md b/plugins/actions/deep-dive/README.md new file mode 100644 index 0000000..5867fce --- /dev/null +++ b/plugins/actions/deep-dive/README.md @@ -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 `` 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 (精读) diff --git a/plugins/actions/deep-dive/README_CN.md b/plugins/actions/deep-dive/README_CN.md new file mode 100644 index 0000000..ca6ac28 --- /dev/null +++ b/plugins/actions/deep-dive/README_CN.md @@ -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. 父文档 `` 标签 + 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` - 中文版 (精读) diff --git a/plugins/actions/deep-dive/deep_dive.png b/plugins/actions/deep-dive/deep_dive.png new file mode 100644 index 0000000..350a69d Binary files /dev/null and b/plugins/actions/deep-dive/deep_dive.png differ diff --git a/plugins/actions/deep-dive/deep_dive.py b/plugins/actions/deep-dive/deep_dive.py new file mode 100644 index 0000000..72372a4 --- /dev/null +++ b/plugins/actions/deep-dive/deep_dive.py @@ -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 = """ + + + + + + + + + +
+ +
+ + + + +""" + +# ================================================================= +# 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 = """ +
+
+
Thinking Process
+

Deep Dive Analysis

+
+ 👤 {user_name} + 📅 {current_date_time_str} + 📊 {word_count} words +
+
+
+ +
+
🔍
+
+
Phase 01
+

The Context

+
{context_html}
+
+
+ + +
+
🧠
+
+
Phase 02
+

The Logic

+
{logic_html}
+
+
+ + +
+
💎
+
+
Phase 03
+

The Insight

+
{insight_html}
+
+
+ + +
+
🚀
+
+
Phase 04
+

The Path

+
{path_html}
+
+
+
+ +
+""" + + +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 '

No context extracted.

' + ) + logic_html = ( + self._process_list_items(logic_md, "logic") + if logic_md + else '

No logic deconstructed.

' + ) + insight_html = ( + self._process_list_items(insight_md, "insight") + if insight_md + else '

No insights found.

' + ) + path_html = ( + self._process_list_items(path_md, "path") + if path_md + else '

No path defined.

' + ) + + 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"

{para_html}

") + 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'
{title}{desc}
' + else: + text_html = self._convert_inline_markdown(text) + path_class = "dd-path-item" if section_type == "path" else "" + item_html = ( + f'
{text_html}
' + ) + 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"

{para_html}

") + current_paragraph = [] + + # Flush remaining paragraph + if current_paragraph: + para_text = " ".join(current_paragraph) + para_html = self._convert_inline_markdown(para_text) + items.append(f"

{para_html}

") + + if items: + return f'
{" ".join(items)}
' + return f'

No items found.

' + + def _convert_inline_markdown(self, text: str) -> str: + """Convert inline markdown (bold, italic, code) to HTML.""" + # Convert inline code: `code` -> code + text = re.sub(r"`([^`]+)`", r"\1", text) + # Convert bold: **text** -> text + text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) + # Convert italic: *text* -> text (but not inside **) + text = re.sub(r"(?\1", 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*[\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 "" 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'
\n{new_content}\n
' + + if new_styles: + base_html = base_html.replace( + "/* STYLES_INSERTION_POINT */", + f"{new_styles}\n/* STYLES_INSERTION_POINT */", + ) + + base_html = base_html.replace( + "", + f"{wrapped}\n", + ) + + 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*([\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 diff --git a/plugins/actions/deep-dive/deep_dive_cn.png b/plugins/actions/deep-dive/deep_dive_cn.png new file mode 100644 index 0000000..fcf4c78 Binary files /dev/null and b/plugins/actions/deep-dive/deep_dive_cn.png differ diff --git a/plugins/actions/deep-dive/deep_dive_cn.py b/plugins/actions/deep-dive/deep_dive_cn.py new file mode 100644 index 0000000..a557fd3 --- /dev/null +++ b/plugins/actions/deep-dive/deep_dive_cn.py @@ -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 = """ + + + + + + + + + +
+ +
+ + + + +""" + +# ================================================================= +# 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 = """ +
+
+
思维过程
+

精读分析报告

+
+ 👤 {user_name} + 📅 {current_date_time_str} + 📊 {word_count} 字 +
+
+
+ +
+
🔍
+
+
Phase 01
+

全景 (The Context)

+
{context_html}
+
+
+ + +
+
🧠
+
+
Phase 02
+

脉络 (The Logic)

+
{logic_html}
+
+
+ + +
+
💎
+
+
Phase 03
+

洞察 (The Insight)

+
{insight_html}
+
+
+ + +
+
🚀
+
+
Phase 04
+

路径 (The Path)

+
{path_html}
+
+
+
+ +
+""" + + +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 '

未能提取全景信息。

' + ) + logic_html = ( + self._process_list_items(logic_md, "logic") + if logic_md + else '

未能解构脉络。

' + ) + insight_html = ( + self._process_list_items(insight_md, "insight") + if insight_md + else '

未能发现洞察。

' + ) + path_html = ( + self._process_list_items(path_md, "path") + if path_md + else '

未能定义路径。

' + ) + + 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"

{para_html}

") + 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'
{title}{desc}
' + else: + text_html = self._convert_inline_markdown(text) + path_class = "dd-path-item" if section_type == "path" else "" + item_html = ( + f'
{text_html}
' + ) + 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"

{para_html}

") + current_paragraph = [] + + # 清空剩余段落 + if current_paragraph: + para_text = " ".join(current_paragraph) + para_html = self._convert_inline_markdown(para_text) + items.append(f"

{para_html}

") + + if items: + return f'
{" ".join(items)}
' + return f'

未找到条目。

' + + def _convert_inline_markdown(self, text: str) -> str: + """将行内 markdown(粗体、斜体、代码)转换为 HTML。""" + # 转换行内代码:`code` -> code + text = re.sub(r"`([^`]+)`", r"\1", text) + # 转换粗体:**text** -> text + text = re.sub(r"\*\*(.+?)\*\*", r"\1", text) + # 转换斜体:*text* -> text(但不在 ** 内部) + text = re.sub(r"(?\1", 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*[\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 "" 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'
\n{new_content}\n
' + + if new_styles: + base_html = base_html.replace( + "/* STYLES_INSERTION_POINT */", + f"{new_styles}\n/* STYLES_INSERTION_POINT */", + ) + + base_html = base_html.replace( + "", + f"{wrapped}\n", + ) + + 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*([\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