From 56a6ddd42291e72b17694749c3fbfc2c7951a2f1 Mon Sep 17 00:00:00 2001 From: fujie Date: Wed, 4 Mar 2026 23:14:06 +0800 Subject: [PATCH] feat(smart-mind-map-tool): initial release v1.0.0 with adaptive Rich UI --- .agent/skills/README.md | 71 + .agent/skills/community-announcer/SKILL.md | 23 + .agent/skills/doc-mirror-sync/SKILL.md | 50 + .agent/skills/doc-mirror-sync/scripts/sync.py | 38 + .agent/skills/gh-issue-replier/SKILL.md | 51 + .../references/example_reference.md | 17 + .../gh-issue-replier/references/templates.md | 45 + .../gh-issue-replier/scripts/check_star.sh | 31 + .agent/skills/gh-issue-scheduler/SKILL.md | 42 + .../scripts/find_unanswered.sh | 42 + .agent/skills/i18n-validator/SKILL.md | 14 + .../i18n-validator/scripts/validate_i18n.py | 54 + .agent/skills/plugin-scaffolder/SKILL.md | 19 + .../assets/README_template.md | 34 + .../plugin-scaffolder/assets/template.py | 80 + .../plugin-scaffolder/assets/template.py.j2 | 80 + .../plugin-scaffolder/scripts/scaffold.py | 66 + .agent/skills/pr-reviewer/SKILL.md | 180 ++ .agent/skills/pr-submitter/SKILL.md | 194 ++ .agent/skills/release-finalizer/SKILL.md | 208 ++ .agent/skills/release-prep/SKILL.md | 157 ++ .agent/skills/source-code-analyzer/SKILL.md | 31 + .agent/skills/version-bumper/SKILL.md | 26 + .agent/skills/version-bumper/scripts/bump.py | 70 + README.md | 7 +- README_CN.md | 7 +- docs/plugins/tools/index.md | 1 + docs/plugins/tools/index.zh.md | 1 + docs/plugins/tools/smart-mind-map-tool.md | 62 + docs/plugins/tools/smart-mind-map-tool.zh.md | 62 + .../github_copilot_sdk_files_filter_cn.py | 2 +- plugins/tools/smart-mind-map-tool/README.md | 62 + .../tools/smart-mind-map-tool/README_CN.md | 62 + .../smart_mind_map_tool.py | 1802 +++++++++++++++-- plugins/tools/smart-mind-map-tool/v1.0.0.md | 23 + .../tools/smart-mind-map-tool/v1.0.0_CN.md | 23 + 36 files changed, 3550 insertions(+), 187 deletions(-) create mode 100644 .agent/skills/README.md create mode 100644 .agent/skills/community-announcer/SKILL.md create mode 100644 .agent/skills/doc-mirror-sync/SKILL.md create mode 100644 .agent/skills/doc-mirror-sync/scripts/sync.py create mode 100644 .agent/skills/gh-issue-replier/SKILL.md create mode 100644 .agent/skills/gh-issue-replier/references/example_reference.md create mode 100644 .agent/skills/gh-issue-replier/references/templates.md create mode 100755 .agent/skills/gh-issue-replier/scripts/check_star.sh create mode 100644 .agent/skills/gh-issue-scheduler/SKILL.md create mode 100755 .agent/skills/gh-issue-scheduler/scripts/find_unanswered.sh create mode 100644 .agent/skills/i18n-validator/SKILL.md create mode 100644 .agent/skills/i18n-validator/scripts/validate_i18n.py create mode 100644 .agent/skills/plugin-scaffolder/SKILL.md create mode 100644 .agent/skills/plugin-scaffolder/assets/README_template.md create mode 100644 .agent/skills/plugin-scaffolder/assets/template.py create mode 100644 .agent/skills/plugin-scaffolder/assets/template.py.j2 create mode 100644 .agent/skills/plugin-scaffolder/scripts/scaffold.py create mode 100644 .agent/skills/pr-reviewer/SKILL.md create mode 100644 .agent/skills/pr-submitter/SKILL.md create mode 100644 .agent/skills/release-finalizer/SKILL.md create mode 100644 .agent/skills/release-prep/SKILL.md create mode 100644 .agent/skills/source-code-analyzer/SKILL.md create mode 100644 .agent/skills/version-bumper/SKILL.md create mode 100644 .agent/skills/version-bumper/scripts/bump.py create mode 100644 docs/plugins/tools/smart-mind-map-tool.md create mode 100644 docs/plugins/tools/smart-mind-map-tool.zh.md create mode 100644 plugins/tools/smart-mind-map-tool/README.md create mode 100644 plugins/tools/smart-mind-map-tool/README_CN.md create mode 100644 plugins/tools/smart-mind-map-tool/v1.0.0.md create mode 100644 plugins/tools/smart-mind-map-tool/v1.0.0_CN.md diff --git a/.agent/skills/README.md b/.agent/skills/README.md new file mode 100644 index 0000000..27a62de --- /dev/null +++ b/.agent/skills/README.md @@ -0,0 +1,71 @@ +# Agent Skills Index + +This folder contains reusable Agent Skills for GitHub Copilot / VS Code custom agent workflows. + +## Available Skills + +- **community-announcer** + - Purpose: Generate community announcement content and related assets. + - Entry: `community-announcer/SKILL.md` + +- **doc-mirror-sync** + - Purpose: Sync mirrored documentation content and helper scripts. + - Entry: `doc-mirror-sync/SKILL.md` + +- **gh-issue-replier** + - Purpose: Draft standardized issue replies with templates. + - Entry: `gh-issue-replier/SKILL.md` + +- **gh-issue-scheduler** + - Purpose: Schedule and discover unanswered issues for follow-up. + - Entry: `gh-issue-scheduler/SKILL.md` + +- **i18n-validator** + - Purpose: Validate translation key consistency across i18n dictionaries. + - Entry: `i18n-validator/SKILL.md` + +- **plugin-scaffolder** + - Purpose: Scaffold OpenWebUI plugin boilerplate with repository standards. + - Entry: `plugin-scaffolder/SKILL.md` + +- **version-bumper** + - Purpose: Assist with semantic version bumping workflows. + - Entry: `version-bumper/SKILL.md` + +- **xlsx-single-file** + - Purpose: Single-file spreadsheet operations workflow without LibreOffice. + - Entry: `xlsx-single-file/SKILL.md` + +--- + +## Release Pipeline Skills + +These four skills form a complete release pipeline and are designed to be used in sequence: + +``` +release-prep → pr-submitter → pr-reviewer → release-finalizer + (prepare) (push & PR) (respond to review) (merge & close issue) +``` + +- **release-prep** + - Purpose: Full release preparation — version sync across 7+ files, bilingual release notes creation, consistency check, and commit. + - Entry: `release-prep/SKILL.md` + +- **pr-submitter** + - Purpose: Shell-escape-safe PR submission — writes body to temp file, validates sections, pushes branch, creates PR via `gh pr create --body-file`. + - Entry: `pr-submitter/SKILL.md` + +- **pr-reviewer** + - Purpose: Fetch PR review comments, categorize feedback, implement fixes, commit and push, reply to reviewers. + - Entry: `pr-reviewer/SKILL.md` + +- **release-finalizer** + - Purpose: Merge release PR to main with proper commit message, auto-link and close related issues, post closing messages. + - Entry: `release-finalizer/SKILL.md` + +## Notes + +- Skill definitions follow the expected location pattern: + - `.github/skills//SKILL.md` +- Each skill may include optional `assets/`, `references/`, and `scripts/` folders. +- This directory mirrors `.gemini/skills` for compatibility. diff --git a/.agent/skills/community-announcer/SKILL.md b/.agent/skills/community-announcer/SKILL.md new file mode 100644 index 0000000..71261b8 --- /dev/null +++ b/.agent/skills/community-announcer/SKILL.md @@ -0,0 +1,23 @@ +--- +name: community-announcer +description: Drafts engaging English and Chinese update announcements for the OpenWebUI Community and other social platforms. Use when a new version is released. +--- + +# Community Announcer + +## Overview +Automates the drafting of high-impact update announcements. + +## Workflow +1. **Source Intel**: Read the latest version's `What's New` section from `README.md`. +2. **Drafting**: Create two versions: + - **Community Post**: Professional, structured, technical. + - **Catchy Short**: For Discord/Twitter, use emojis and bullet points. +3. **Multi-language**: Generate BOTH English and Chinese versions automatically. + +## Announcement Structure (Recommended) +- **Headline**: "Update vX.X.X - [Main Feature]" +- **Introduction**: Brief context. +- **Key Highlights**: Bulleted list of fixes/features. +- **Action**: "Download from [Market Link]" +- **Closing**: Thanks and Star request. diff --git a/.agent/skills/doc-mirror-sync/SKILL.md b/.agent/skills/doc-mirror-sync/SKILL.md new file mode 100644 index 0000000..c83492e --- /dev/null +++ b/.agent/skills/doc-mirror-sync/SKILL.md @@ -0,0 +1,50 @@ +--- +name: doc-mirror-sync +description: Automatically synchronizes plugin READMEs to the official documentation directory (docs/). Use after editing a plugin's local documentation to keep the MkDocs site up to date. +--- + +# Doc Mirror Sync + +## Overview +Automates the mirroring of `plugins/{type}/{name}/README.md` to `docs/plugins/{type}/{name}.md`. + +## Docs-Only Mode (No Release Changes) +Use this mode when the request is "only sync docs". + +- Only update documentation mirror files under `docs/plugins/**`. +- Do **not** bump plugin version. +- Do **not** modify plugin code (`plugins/**.py`) unless explicitly requested. +- Do **not** update root badges/dates for release. +- Do **not** run release preparation steps. + +## Workflow +1. Identify changed READMEs. +2. Copy content to corresponding mirror paths. +3. Update version badges in `docs/plugins/{type}/index.md`. + +## Commands + +### Sync all mirrors (EN + ZH) + +```bash +python .github/skills/doc-mirror-sync/scripts/sync.py +``` + +### Sync only one plugin (EN only) + +```bash +cp plugins///README.md docs/plugins//.md +``` + +### Sync only one plugin (EN + ZH) + +```bash +cp plugins///README.md docs/plugins//.md +cp plugins///README_CN.md docs/plugins//.zh.md +``` + +## Notes + +- If asked for English-only update, sync only `README.md` -> `.md` mirror. +- If both languages are requested, sync both `README.md` and `README_CN.md`. +- After syncing, verify git diff only contains docs file changes. diff --git a/.agent/skills/doc-mirror-sync/scripts/sync.py b/.agent/skills/doc-mirror-sync/scripts/sync.py new file mode 100644 index 0000000..508674a --- /dev/null +++ b/.agent/skills/doc-mirror-sync/scripts/sync.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +import os +import shutil +import re + +def sync_mirrors(): + plugins_root = "plugins" + docs_root = "docs/plugins" + + types = ["actions", "filters", "pipes", "pipelines", "tools"] + + for t in types: + src_type_dir = os.path.join(plugins_root, t) + dest_type_dir = os.path.join(docs_root, t) + + if not os.path.exists(src_type_dir): continue + os.makedirs(dest_type_dir, exist_ok=True) + + for name in os.listdir(src_type_dir): + plugin_dir = os.path.join(src_type_dir, name) + if not os.path.isdir(plugin_dir): continue + + # Sync README.md -> docs/plugins/{type}/{name}.md + src_readme = os.path.join(plugin_dir, "README.md") + if os.path.exists(src_readme): + dest_readme = os.path.join(dest_type_dir, f"{name}.md") + shutil.copy(src_readme, dest_readme) + print(f"✅ Mirrored: {t}/{name} (EN)") + + # Sync README_CN.md -> docs/plugins/{type}/{name}.zh.md + src_readme_cn = os.path.join(plugin_dir, "README_CN.md") + if os.path.exists(src_readme_cn): + dest_readme_zh = os.path.join(dest_type_dir, f"{name}.zh.md") + shutil.copy(src_readme_cn, dest_readme_zh) + print(f"✅ Mirrored: {t}/{name} (ZH)") + +if __name__ == "__main__": + sync_mirrors() diff --git a/.agent/skills/gh-issue-replier/SKILL.md b/.agent/skills/gh-issue-replier/SKILL.md new file mode 100644 index 0000000..0873575 --- /dev/null +++ b/.agent/skills/gh-issue-replier/SKILL.md @@ -0,0 +1,51 @@ +--- +name: gh-issue-replier +description: Professional English replier for GitHub issues. Use when a task is completed, a bug is fixed, or more info is needed from the user. Automates replying using the 'gh' CLI tool. +--- + +# Gh Issue Replier + +## Overview + +The `gh-issue-replier` skill enables Gemini CLI to interact with GitHub issues professionally. It enforces English for all communications and leverages the `gh` CLI to post comments. + +## Workflow + +1. **Identify the Issue**: Find the issue number (e.g., #49). +2. **Check Star Status**: Run the bundled script to check if the author has starred the repo. + * Command: `bash scripts/check_star.sh ` + * Interpretation: + * Exit code **0**: User has starred. Use "Already Starred" templates. + * Exit code **1**: User has NOT starred. Include "Star Request" in the reply. +3. **Select a Template**: Load [templates.md](references/templates.md) to choose a suitable English response pattern. +4. **Draft the Reply**: Compose a concise message based on the star status. +5. **Post the Comment**: Use the `gh` tool to submit the reply. + +## Tool Integration + +### Check Star Status +```bash +bash scripts/check_star.sh +``` + +### Post Comment +```bash +gh issue comment --body "" +``` + +Example (if user has NOT starred): +```bash +gh issue comment 49 --body "This has been fixed in v1.2.7. If you find this helpful, a star on the repo would be much appreciated! ⭐" +``` + +Example (if user HAS starred): +```bash +gh issue comment 49 --body "This has been fixed in v1.2.7. Thanks for your support!" +``` + +## Guidelines + +- **Language**: ALWAYS use English for the comment body, even if the system prompt or user conversation is in another language. +- **Tone**: Professional, helpful, and appreciative. +- **Precision**: When announcing a fix, mention the specific version or the logic change (e.g., "Updated regex pattern"). +- **Closing**: If the issue is resolved and you have permission, you can also use `gh issue close `. diff --git a/.agent/skills/gh-issue-replier/references/example_reference.md b/.agent/skills/gh-issue-replier/references/example_reference.md new file mode 100644 index 0000000..2bc2bd5 --- /dev/null +++ b/.agent/skills/gh-issue-replier/references/example_reference.md @@ -0,0 +1,17 @@ +# Reference Documentation for Gh Issue Replier + +This is a placeholder for detailed reference documentation. +Replace with actual reference content or delete if not needed. + +## Structure Suggestions + +### API Reference Example +- Overview +- Authentication +- Endpoints with examples +- Error codes + +### Workflow Guide Example +- Prerequisites +- Step-by-step instructions +- Best practices diff --git a/.agent/skills/gh-issue-replier/references/templates.md b/.agent/skills/gh-issue-replier/references/templates.md new file mode 100644 index 0000000..2aa7438 --- /dev/null +++ b/.agent/skills/gh-issue-replier/references/templates.md @@ -0,0 +1,45 @@ +# Issue Reply Templates + +Use these templates to craft professional English replies. Adjust placeholders like `@username`, `v1.2.x`, and `[commit hash]` as needed. + +## 1. Acknowledging a New Issue +Use when you first see an issue and want to let the user know you are working on it. + +- "Thank you for reporting this! I'm looking into it right now." +- "Thanks for bringing this to my attention. I'll try to reproduce this behavior and get back to you shortly." + +## 2. Requesting More Information +Use when you need logs or specific details to fix the bug. + +- "Could you please provide the **'Original'** vs **'Normalized'** content from your browser console logs (F12)? It would help a lot in debugging." +- "It would be very helpful if you could share the specific Markdown text that triggered this issue." + +## 3. Announcing a Fix +Use when you have pushed the fix to the repository. + +- "This has been fixed in version **v1.2.x**. You can update the plugin to resolve it." +- "I've just pushed a fix for this in [commit hash]. Please let me know if it works for you after updating." +- "The issue was caused by a greedy regex pattern. I've updated it to use a tempered greedy token to prevent incorrect merging." + +## 4. Guiding to Official Market +Always provide the official market link to ensure the user gets the latest verified version. + +- "The fix is now live! You can download the latest version from the official OpenWebUI Community page here: [Plugin Market Link]. Simply update the function in your OpenWebUI instance to apply the changes." +- "I recommend getting the updated version from the official store: [Link]. It includes the fix for the spacing issue we discussed." + +## 5. Closing the Issue +Use when the issue is confirmed resolved. + +- "Glad to hear it's working now! Closing this for now. Feel free to reopen it if the problem persists." +- "Since this is resolved, I'm closing this issue. Thanks again for your feedback!" + +## 5. Pro-tip: Star Request +Gently handle star requests based on the user's current status. + +### If User has NOT starred: +- "If you find this plugin helpful, a star on the repo would be much appreciated! ⭐" +- "We'd love your support! If this fixed your issue, please consider starring the repository. ⭐" + +### If User HAS already starred: +- "Thanks again for starring the project and for your continuous support!" +- "I appreciate your support and for being a stargazer of this project!" diff --git a/.agent/skills/gh-issue-replier/scripts/check_star.sh b/.agent/skills/gh-issue-replier/scripts/check_star.sh new file mode 100755 index 0000000..16b0a23 --- /dev/null +++ b/.agent/skills/gh-issue-replier/scripts/check_star.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# Robust Star Checker v2 +# Usage: ./check_star.sh + +ISSUE_NUM=$1 +if [ -z "$ISSUE_NUM" ]; then exit 2; fi + +# 1. Get Repo and Author info +REPO_FULL=$(gh repo view --json owner,name -q ".owner.login + \"/\" + .name") +USER_LOGIN=$(gh issue view "$ISSUE_NUM" --json author -q ".author.login") + +# 2. Use GraphQL for high precision (Detects stars even when REST 404s) +IS_STARRED=$(gh api graphql -f query=' +query($owner:String!, $repo:String!, $user:String!) { + repository(owner:$owner, name:$repo) { + stargazers(query:$user, first:1) { + nodes { + login + } + } + } +}' -f owner="${REPO_FULL%/*}" -f repo="${REPO_FULL#*/}" -f user="$USER_LOGIN" -q ".data.repository.stargazers.nodes[0].login") + +if [ "$IS_STARRED" == "$USER_LOGIN" ]; then + echo "Confirmed: @$USER_LOGIN HAS starred $REPO_FULL. ⭐" + exit 0 +else + echo "Confirmed: @$USER_LOGIN has NOT starred $REPO_FULL." + exit 1 +fi diff --git a/.agent/skills/gh-issue-scheduler/SKILL.md b/.agent/skills/gh-issue-scheduler/SKILL.md new file mode 100644 index 0000000..b0ac8db --- /dev/null +++ b/.agent/skills/gh-issue-scheduler/SKILL.md @@ -0,0 +1,42 @@ +--- +name: gh-issue-scheduler +description: Finds all open GitHub issues that haven't been replied to by the owner, summarizes them, and generates a solution plan. Use when the user wants to audit pending tasks or plan maintenance work. +--- + +# Gh Issue Scheduler + +## Overview + +The `gh-issue-scheduler` skill helps maintainers track community feedback by identifying unaddressed issues and drafting actionable technical plans to resolve them. + +## Workflow + +1. **Identify Unanswered Issues**: Run the bundled script to fetch issues without owner replies. + * Command: `bash scripts/find_unanswered.sh` +2. **Analyze and Summarize**: For each identified issue, summarize the core problem and the user's intent. +3. **Generate Solution Plans**: Draft a technical "Action Plan" for each issue, including: + * **Root Cause Analysis** (if possible) + * **Proposed Fix/Implementation** + * **Verification Strategy** +4. **Present to User**: Display a structured report of all pending issues and their respective plans. + +## Tool Integration + +### Find Unanswered Issues +```bash +bash scripts/find_unanswered.sh +``` + +## Report Format + +When presenting the summary, use the following Markdown structure: + +### 📋 Unanswered Issues Audit + +#### Issue #[Number]: [Title] +- **Author**: @username +- **Summary**: Concise description of the problem. +- **Action Plan**: + 1. Step 1 (e.g., Investigate file X) + 2. Step 2 (e.g., Apply fix Y) + 3. Verification (e.g., Run test Z) diff --git a/.agent/skills/gh-issue-scheduler/scripts/find_unanswered.sh b/.agent/skills/gh-issue-scheduler/scripts/find_unanswered.sh new file mode 100755 index 0000000..a423feb --- /dev/null +++ b/.agent/skills/gh-issue-scheduler/scripts/find_unanswered.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +# Fetch all open issues and filter for those without responses from the owner/collaborators. +# Uses 'gh' CLI. + +REPO_FULL=$(gh repo view --json owner,name -q ".owner.login + "/" + .name") +OWNER=${REPO_FULL%/*} + +# 1. Get all open issues +OPEN_ISSUES=$(gh issue list --state open --json number,title,author,createdAt --limit 100) + +echo "Analysis for repository: $REPO_FULL" +echo "------------------------------------" + +# Process each issue +echo "$OPEN_ISSUES" | jq -c '.[]' | while read -r issue; do + NUMBER=$(echo "$issue" | jq -r '.number') + TITLE=$(echo "$issue" | jq -r '.title') + AUTHOR=$(echo "$issue" | jq -r '.author.login') + + # Check comments for owner responses + # We look for comments where the author is the repo owner + COMMENTS=$(gh issue view "$NUMBER" --json comments -q ".comments[].author.login" 2>/dev/null) + + HAS_OWNER_REPLY=false + for COMMENT_AUTHOR in $COMMENTS; do + if [ "$COMMENT_AUTHOR" == "$OWNER" ]; then + HAS_OWNER_REPLY=true + break + fi + done + + if [ "$HAS_OWNER_REPLY" == "false" ]; then + echo "ISSUE_START" + echo "ID: $NUMBER" + echo "Title: $TITLE" + echo "Author: $AUTHOR" + echo "Description:" + gh issue view "$NUMBER" --json body -q ".body" + echo "ISSUE_END" + fi +done diff --git a/.agent/skills/i18n-validator/SKILL.md b/.agent/skills/i18n-validator/SKILL.md new file mode 100644 index 0000000..5b302de --- /dev/null +++ b/.agent/skills/i18n-validator/SKILL.md @@ -0,0 +1,14 @@ +--- +name: i18n-validator +description: Validates multi-language consistency in the TRANSLATIONS dictionary of a plugin. Use to check if any language keys are missing or if translations need updating. +--- + +# I18n Validator + +## Overview +Ensures all 12 supported languages (en-US, zh-CN, etc.) have aligned translation keys. + +## Features +- Detects missing keys in non-English dictionaries. +- Suggests translations using the core AI engine. +- Validates the `fallback_map` for variant redirects. diff --git a/.agent/skills/i18n-validator/scripts/validate_i18n.py b/.agent/skills/i18n-validator/scripts/validate_i18n.py new file mode 100644 index 0000000..8d04d48 --- /dev/null +++ b/.agent/skills/i18n-validator/scripts/validate_i18n.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +import sys +import ast +import os + +def check_i18n(file_path): + if not os.path.exists(file_path): + print(f"Error: File not found {file_path}") + return + + with open(file_path, 'r', encoding='utf-8') as f: + tree = ast.parse(f.read()) + + translations = {} + for node in tree.body: + if isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name) and target.id == "TRANSLATIONS": + translations = ast.literal_eval(node.value) + break + + if not translations: + print("⚠️ No TRANSLATIONS dictionary found.") + return + + # Base keys from English + base_lang = "en-US" + if base_lang not in translations: + print(f"❌ Error: {base_lang} missing in TRANSLATIONS.") + return + + base_keys = set(translations[base_lang].keys()) + print(f"🔍 Analyzing {file_path}...") + print(f"Standard keys ({len(base_keys)}): {', '.join(sorted(base_keys))} +") + + for lang, keys in translations.items(): + if lang == base_lang: continue + lang_keys = set(keys.keys()) + missing = base_keys - lang_keys + extra = lang_keys - base_keys + + if missing: + print(f"❌ {lang}: Missing {len(missing)} keys: {', '.join(missing)}") + if extra: + print(f"⚠️ {lang}: Has {len(extra)} extra keys: {', '.join(extra)}") + if not missing and not extra: + print(f"✅ {lang}: Aligned.") + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: validate_i18n.py ") + sys.exit(1) + check_i18n(sys.argv[1]) diff --git a/.agent/skills/plugin-scaffolder/SKILL.md b/.agent/skills/plugin-scaffolder/SKILL.md new file mode 100644 index 0000000..00bd8e3 --- /dev/null +++ b/.agent/skills/plugin-scaffolder/SKILL.md @@ -0,0 +1,19 @@ +--- +name: plugin-scaffolder +description: Generates a standardized single-file i18n Python plugin template based on project standards. Use when starting a new plugin development to skip boilerplate writing. +--- + +# Plugin Scaffolder + +## Overview +Generates compliant OpenWebUI plugin templates with built-in i18n, common utility methods, and required docstring fields. + +## Usage +1. Provide the **Plugin Name** and **Type** (action/filter/pipe). +2. The skill will generate the `.py` file and the bilingual `README` files. + +## Template Standard +- `Valves(BaseModel)` with `UPPER_SNAKE_CASE` +- `_get_user_context` with JS fallback and timeout +- `_emit_status` and `_emit_debug_log` methods +- Standardized docstring metadata diff --git a/.agent/skills/plugin-scaffolder/assets/README_template.md b/.agent/skills/plugin-scaffolder/assets/README_template.md new file mode 100644 index 0000000..17d93e7 --- /dev/null +++ b/.agent/skills/plugin-scaffolder/assets/README_template.md @@ -0,0 +1,34 @@ +# {{TITLE}} + +**Author:** [Fu-Jie](https://github.com/Fu-Jie/openwebui-extensions) | **Version:** 0.1.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT + +{{DESCRIPTION}} + +## 🔥 What's New in v0.1.0 + +* Initial release of {{TITLE}}. + +## 🌐 Multilingual Support + +Supports automatic interface and status switching for the following languages: +`English`, `简体中文`, `繁體中文 (香港)`, `繁體中文 (台灣)`, `한국어`, `日本語`, `Français`, `Deutsch`, `Español`, `Italiano`, `Tiếng Việt`, `Bahasa Indonesia`. + +## ✨ Core Features + +* Feature 1 +* Feature 2 + +## How to Use 🛠️ + +1. Install the plugin in Open WebUI. +2. Configure settings in Valves. + +## Configuration (Valves) ⚙️ + +| Parameter | Default | Description | +| :--- | :--- | :--- | +| `priority` | `50` | Execution priority. | + +## ⭐ Support + +If this plugin has been useful, a star on [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) is a big motivation for me. Thank you for the support. diff --git a/.agent/skills/plugin-scaffolder/assets/template.py b/.agent/skills/plugin-scaffolder/assets/template.py new file mode 100644 index 0000000..3077c65 --- /dev/null +++ b/.agent/skills/plugin-scaffolder/assets/template.py @@ -0,0 +1,80 @@ +""" +title: {{TITLE}} +author: Fu-Jie +author_url: https://github.com/Fu-Jie/openwebui-extensions +funding_url: https://github.com/open-webui +version: 0.1.0 +description: {{DESCRIPTION}} +""" + +import asyncio +import logging +import json +from typing import Optional, Dict, Any, List, Callable, Awaitable +from pydantic import BaseModel, Field +from fastapi import Request + +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + +TRANSLATIONS = { + "en-US": {"status_starting": "Starting {{TITLE}}..."}, + "zh-CN": {"status_starting": "正在启动 {{TITLE}}..."}, + "zh-HK": {"status_starting": "正在啟動 {{TITLE}}..."}, + "zh-TW": {"status_starting": "正在啟動 {{TITLE}}..."}, + "ko-KR": {"status_starting": "{{TITLE}} 시작 중..."}, + "ja-JP": {"status_starting": "{{TITLE}} を起動中..."}, + "fr-FR": {"status_starting": "Démarrage de {{TITLE}}..."}, + "de-DE": {"status_starting": "{{TITLE}} wird gestartet..."}, + "es-ES": {"status_starting": "Iniciando {{TITLE}}..."}, + "it-IT": {"status_starting": "Avvio di {{TITLE}}..."}, + "vi-VN": {"status_starting": "Đang khởi động {{TITLE}}..."}, + "id-ID": {"status_starting": "Memulai {{TITLE}}..."}, +} + +class {{CLASS_NAME}}: + class Valves(BaseModel): + priority: int = Field(default=50, description="Priority level (lower = earlier).") + show_status: bool = Field(default=True, description="Show status updates in UI.") + + def __init__(self): + self.valves = self.Valves() + self.fallback_map = { + "zh": "zh-CN", "en": "en-US", "ko": "ko-KR", "ja": "ja-JP", + "fr": "fr-FR", "de": "de-DE", "es": "es-ES", "it": "it-IT", + "vi": "vi-VN", "id": "id-ID" + } + + def _get_translation(self, lang: str, key: str, **kwargs) -> str: + target_lang = lang + if target_lang not in TRANSLATIONS: + base = target_lang.split("-")[0] + target_lang = self.fallback_map.get(base, "en-US") + + lang_dict = TRANSLATIONS.get(target_lang, TRANSLATIONS["en-US"]) + text = lang_dict.get(key, TRANSLATIONS["en-US"].get(key, key)) + return text.format(**kwargs) if kwargs else text + + async def _get_user_context(self, __user__: Optional[dict], __event_call__: Optional[Callable] = None, __request__: Optional[Request] = None) -> dict: + user_data = __user__ if isinstance(__user__, dict) else {} + user_language = user_data.get("language", "en-US") + if __event_call__: + try: + js = "try { return (document.documentElement.lang || localStorage.getItem('locale') || navigator.language || 'en-US'); } catch (e) { return 'en-US'; }" + frontend_lang = await asyncio.wait_for(__event_call__({"type": "execute", "data": {"code": js}}), timeout=2.0) + if frontend_lang: user_language = frontend_lang + except: pass + return {"user_language": user_language} + + async def {{METHOD_NAME}}(self, body: dict, __user__: Optional[dict] = None, __event_emitter__=None, __event_call__=None, __request__: Optional[Request] = None) -> dict: + if self.valves.show_status and __event_emitter__: + user_ctx = await self._get_user_context(__user__, __event_call__, __request__) + msg = self._get_translation(user_ctx["user_language"], "status_starting") + await __event_emitter__({"type": "status", "data": {"description": msg, "done": False}}) + + # Implement core logic here + + if self.valves.show_status and __event_emitter__: + await __event_emitter__({"type": "status", "data": {"description": "Done", "done": True}}) + return body diff --git a/.agent/skills/plugin-scaffolder/assets/template.py.j2 b/.agent/skills/plugin-scaffolder/assets/template.py.j2 new file mode 100644 index 0000000..3077c65 --- /dev/null +++ b/.agent/skills/plugin-scaffolder/assets/template.py.j2 @@ -0,0 +1,80 @@ +""" +title: {{TITLE}} +author: Fu-Jie +author_url: https://github.com/Fu-Jie/openwebui-extensions +funding_url: https://github.com/open-webui +version: 0.1.0 +description: {{DESCRIPTION}} +""" + +import asyncio +import logging +import json +from typing import Optional, Dict, Any, List, Callable, Awaitable +from pydantic import BaseModel, Field +from fastapi import Request + +# Configure logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + +TRANSLATIONS = { + "en-US": {"status_starting": "Starting {{TITLE}}..."}, + "zh-CN": {"status_starting": "正在启动 {{TITLE}}..."}, + "zh-HK": {"status_starting": "正在啟動 {{TITLE}}..."}, + "zh-TW": {"status_starting": "正在啟動 {{TITLE}}..."}, + "ko-KR": {"status_starting": "{{TITLE}} 시작 중..."}, + "ja-JP": {"status_starting": "{{TITLE}} を起動中..."}, + "fr-FR": {"status_starting": "Démarrage de {{TITLE}}..."}, + "de-DE": {"status_starting": "{{TITLE}} wird gestartet..."}, + "es-ES": {"status_starting": "Iniciando {{TITLE}}..."}, + "it-IT": {"status_starting": "Avvio di {{TITLE}}..."}, + "vi-VN": {"status_starting": "Đang khởi động {{TITLE}}..."}, + "id-ID": {"status_starting": "Memulai {{TITLE}}..."}, +} + +class {{CLASS_NAME}}: + class Valves(BaseModel): + priority: int = Field(default=50, description="Priority level (lower = earlier).") + show_status: bool = Field(default=True, description="Show status updates in UI.") + + def __init__(self): + self.valves = self.Valves() + self.fallback_map = { + "zh": "zh-CN", "en": "en-US", "ko": "ko-KR", "ja": "ja-JP", + "fr": "fr-FR", "de": "de-DE", "es": "es-ES", "it": "it-IT", + "vi": "vi-VN", "id": "id-ID" + } + + def _get_translation(self, lang: str, key: str, **kwargs) -> str: + target_lang = lang + if target_lang not in TRANSLATIONS: + base = target_lang.split("-")[0] + target_lang = self.fallback_map.get(base, "en-US") + + lang_dict = TRANSLATIONS.get(target_lang, TRANSLATIONS["en-US"]) + text = lang_dict.get(key, TRANSLATIONS["en-US"].get(key, key)) + return text.format(**kwargs) if kwargs else text + + async def _get_user_context(self, __user__: Optional[dict], __event_call__: Optional[Callable] = None, __request__: Optional[Request] = None) -> dict: + user_data = __user__ if isinstance(__user__, dict) else {} + user_language = user_data.get("language", "en-US") + if __event_call__: + try: + js = "try { return (document.documentElement.lang || localStorage.getItem('locale') || navigator.language || 'en-US'); } catch (e) { return 'en-US'; }" + frontend_lang = await asyncio.wait_for(__event_call__({"type": "execute", "data": {"code": js}}), timeout=2.0) + if frontend_lang: user_language = frontend_lang + except: pass + return {"user_language": user_language} + + async def {{METHOD_NAME}}(self, body: dict, __user__: Optional[dict] = None, __event_emitter__=None, __event_call__=None, __request__: Optional[Request] = None) -> dict: + if self.valves.show_status and __event_emitter__: + user_ctx = await self._get_user_context(__user__, __event_call__, __request__) + msg = self._get_translation(user_ctx["user_language"], "status_starting") + await __event_emitter__({"type": "status", "data": {"description": msg, "done": False}}) + + # Implement core logic here + + if self.valves.show_status and __event_emitter__: + await __event_emitter__({"type": "status", "data": {"description": "Done", "done": True}}) + return body diff --git a/.agent/skills/plugin-scaffolder/scripts/scaffold.py b/.agent/skills/plugin-scaffolder/scripts/scaffold.py new file mode 100644 index 0000000..09b9a49 --- /dev/null +++ b/.agent/skills/plugin-scaffolder/scripts/scaffold.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +import sys +import os + + +def scaffold(p_type, p_name, title, desc): + target_dir = f"plugins/{p_type}/{p_name}" + os.makedirs(target_dir, exist_ok=True) + + class_name = ( + "Action" + if p_type == "actions" + else ( + "Filter" + if p_type == "filters" + else "Tools" if p_type == "tools" else "Pipe" + ) + ) + method_name = ( + "action" + if p_type == "actions" + else ( + "outlet" + if p_type == "filters" + else "execute" if p_type == "tools" else "pipe" + ) + ) + + replacements = { + "{{TITLE}}": title, + "{{DESCRIPTION}}": desc, + "{{CLASS_NAME}}": class_name, + "{{METHOD_NAME}}": method_name, + } + + # Files to generate + templates = [ + ("assets/template.py.j2", f"{p_name}.py"), + ("assets/README_template.md", "README.md"), + ("assets/README_template.md", "README_CN.md"), + ] + + # Path relative to skill root + skill_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + for t_path, t_name in templates: + template_file = os.path.join(skill_root, t_path) + if not os.path.exists(template_file): + print(f"⚠️ Warning: Template not found {template_file}") + continue + + with open(template_file, "r") as f: + content = f.read() + for k, v in replacements.items(): + content = content.replace(k, v) + + with open(os.path.join(target_dir, t_name), "w") as f: + f.write(content) + print(f"✅ Generated: {target_dir}/{t_name}") + + +if __name__ == "__main__": + if len(sys.argv) < 5: + print("Usage: scaffold.py <desc>") + sys.exit(1) + scaffold(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) diff --git a/.agent/skills/pr-reviewer/SKILL.md b/.agent/skills/pr-reviewer/SKILL.md new file mode 100644 index 0000000..6bb9af9 --- /dev/null +++ b/.agent/skills/pr-reviewer/SKILL.md @@ -0,0 +1,180 @@ +--- +name: pr-reviewer +description: Fetches PR review comments, analyzes requested changes, implements fixes, commits and pushes the resolution. Use after a reviewer has left comments on an open PR to close the feedback loop efficiently. +--- + +# PR Reviewer + +## Overview + +This skill automates the response cycle for code review. When a reviewer leaves comments on a Pull Request, this skill fetches all pending feedback, categorizes issues by severity, implements fixes, and submits a follow-up commit with appropriate review response comments. + +## Prerequisites + +- An open PR exists with pending review comments +- The local branch matches the PR's head branch +- `gh` CLI is authenticated + +--- + +## Workflow + +### Step 1 — Fetch Review State + +Retrieve all review comments and overall review status: + +```bash +# Get overall review decisions +PAGER=cat GH_PAGER=cat gh pr view <PR-NUMBER> --json reviews,reviewDecision,headRefName \ + --jq '{decision: .reviewDecision, reviews: [.reviews[] | {author: .author.login, state: .state, body: .body}]}' + +# Get inline code comments (specific line comments) +PAGER=cat GH_PAGER=cat gh api repos/Fu-Jie/openwebui-extensions/pulls/<PR-NUMBER>/comments \ + --jq '[.[] | {path: .path, line: .line, body: .body, author: .user.login, id: .id}]' + +# Get general issue comments +PAGER=cat GH_PAGER=cat gh issue view <PR-NUMBER> --comments --json comments \ + --jq '[.comments[] | {author: .author.login, body: .body}]' +``` + +Confirm the current local branch matches the PR head: +```bash +git branch --show-current +``` +If mismatched, checkout the correct branch first. + +### Step 2 — Categorize Review Feedback + +Group feedback into categories: + +| Category | Examples | Action | +|----------|---------|--------| +| **Code Bug** | Logic error, incorrect variable, broken condition | Fix code immediately | +| **Style / Formatting** | Indentation, naming convention, missing blank line | Fix code | +| **Documentation** | Missing i18n key, wrong version in README, typo | Fix docs | +| **Design Question** | Suggestion to restructure, alternative approach | Discuss with user before implementing | +| **Nitpick / Optional** | Minor style preferences reviewer marked as optional | Fix if quick; document if skipped | +| **Blocking** | Reviewer explicitly blocks merge | Must fix before proceeding | + +Present the full categorized list to the user and confirm the resolution plan. + +### Step 3 — Implement Fixes + +For each accepted fix: + +1. Read the affected file at the commented line for context: + ```bash + sed -n '<line-5>,<line+10>p' <file-path> + ``` +2. Apply the fix using appropriate file edit tools +3. After editing, verify the specific area looks correct + +**For code changes that might affect behavior:** +- Check if tests exist: `ls tests/test_*.py` +- If tests exist, run them: `python -m pytest tests/ -v` + +**For documentation fixes:** +- If modifying README.md, check if `docs/` mirror needs the same fix +- Apply the same fix to both locations + +### Step 4 — Run Consistency Checks + +After all fixes are applied: + +```bash +# Version consistency (if any version files were touched) +python3 scripts/check_version_consistency.py + +# Quick syntax check for Python files +python3 -m py_compile plugins/{type}/{name}/{name}.py && echo "✅ Syntax OK" +``` + +### Step 5 — Stage and Commit + +Create a new commit (do NOT amend if the branch has already been pushed, to avoid force-push): + +```bash +git add -A +git status +``` + +Draft a Conventional Commits message for the fixup: + +Format: `fix(scope): address review feedback` + +Body should list what was fixed, referencing reviewer concerns: +``` +fix(github-copilot-sdk): address review feedback from @reviewer + +- Fix X per review comment on line Y of file Z +- Update README to clarify auth requirement +- Correct edge case in _parse_mcp_servers logic +``` + +```bash +git commit -m "<fixup commit message>" +``` + +### Step 6 — Push the Fix Commit + +```bash +git push origin $(git branch --show-current) +``` + +**Force-push policy:** +- Use `git push` (non-force) by default +- Only use `git push --force-with-lease` if: + 1. The user explicitly requests it, AND + 2. The only change is an amended commit squash (cosmetic, no logic change) + 3. Never use `--force` (without `--lease`) + +### Step 7 — Respond to Reviewers + +For each addressed review comment, post a reply: + +```bash +# Reply to inline comment +gh api repos/Fu-Jie/openwebui-extensions/pulls/<PR-NUMBER>/comments/<COMMENT-ID>/replies \ + -X POST -f body="Fixed in commit <SHORT-SHA>. <Brief explanation of what was changed.>" + +# General comment to summarize all fixes +gh issue comment <PR-NUMBER> --body "All review feedback addressed in commit <SHORT-SHA>: +- Fixed: <item 1> +- Fixed: <item 2> +Ready for re-review. 🙏" +``` + +### Step 8 — Re-Request Review (Optional) + +If the reviewer had submitted a `CHANGES_REQUESTED` review, request a new review after fixes: + +```bash +PAGER=cat GH_PAGER=cat gh api repos/Fu-Jie/openwebui-extensions/pulls/<PR-NUMBER>/requested_reviewers \ + -X POST -f reviewers[]='<reviewer-login>' +``` + +--- + +## Decision Guide + +### When NOT to implement a suggestion immediately + +- **Design questions**: "Should this be a separate class?" — Present to user for decision +- **Optional nitpicks**: Reviewer marked as `nit:` — Ask user if they want to include it +- **Large refactors**: If fix would require changing >50 lines, propose a separate follow-up issue instead + +### When to ask the user before proceeding + +- Any fix involving behavioral changes to plugin logic +- Renaming Valve keys (breaking change — requires migration notes) +- Changes that affect the bilingual release notes already committed + +--- + +## Anti-Patterns to Avoid + +- ❌ Do NOT `git commit --amend` on a pushed commit without user approval for force-push +- ❌ Do NOT silently skip a reviewer's comment; always acknowledge it (implement or explain why not) +- ❌ Do NOT use `--force` (only `--force-with-lease` when absolutely necessary) +- ❌ Do NOT make unrelated changes in the fixup commit; keep scope focused on review feedback +- ❌ Do NOT respond to reviewer comments in Chinese if the PR language context is English diff --git a/.agent/skills/pr-submitter/SKILL.md b/.agent/skills/pr-submitter/SKILL.md new file mode 100644 index 0000000..7dafc3b --- /dev/null +++ b/.agent/skills/pr-submitter/SKILL.md @@ -0,0 +1,194 @@ +--- +name: pr-submitter +description: Submits a feature branch as a Pull Request with a validated, properly formatted bilingual PR body. Handles shell-escape-safe body writing via temp files. Use after release-prep has committed all changes. +--- + +# PR Submitter + +## Overview + +This skill handles the final step of pushing a feature branch and creating a validated Pull Request on GitHub. Its primary purpose is to avoid the shell-escaping pitfalls (backticks, special characters in `gh pr create --body`) by always writing the PR body to a **temp file** first. + +## Prerequisites + +- All changes are committed (use `release-prep` skill first) +- The `gh` CLI is authenticated (`gh auth status`) +- Current branch is NOT `main` or `master` + +--- + +## Workflow + +### Step 0 — Initialize Temp Directory (Project-Based) + +For all temporary files, use the project's `.temp/` directory instead of system `/tmp`: + +```bash +# Create temp directory if it doesn't exist +mkdir -p .temp +``` + +**Why**: All temporary files stay within the project workspace, avoiding system `/tmp` pollution and better aligning with OpenWebUI workspace isolation principles. + +### Step 1 — Pre-Flight Checks + +Run these checks before any push: + +```bash +# 1. Confirm not on protected branch +git branch --show-current + +# 2. Verify there are commits to push +git log origin/$(git branch --show-current)..HEAD --oneline 2>/dev/null || echo "No remote tracking branch yet" + +# 3. Check gh CLI auth +gh auth status +``` + +If any check fails, stop and report clearly. + +### Step 2 — Collect PR Metadata + +Gather: +- **PR Title**: Must follow Conventional Commits format, English only (e.g., `feat(github-copilot-sdk): release v0.8.0 with conditional tool filtering`) +- **Target base branch**: Default is `main` +- **Plugin name + version** (to build body sections) +- **Key changes** (reuse from release-prep or the latest What's New section) + +### Step 3 — Build PR Body File (Shell-Escape-Safe) + +**Always write the body to a temp file in `.temp/` directory.** Never embed multi-line markdown with special characters directly in a shell command. + +```bash +cat > .temp/pr_body.md << 'HEREDOC' +## Summary + +Brief one-sentence description of what this PR accomplishes. + +## Changes + +### New Features +- Feature 1 description +- Feature 2 description + +### Bug Fixes +- Fix 1 description + +## Plugin Version +- `PluginName` bumped to `vX.X.X` + +## Documentation +- README.md / README_CN.md updated +- docs/ mirrors synced + +## Testing +- [ ] Tested locally in OpenWebUI +- [ ] i18n validated (all language keys present) +- [ ] Version consistency check passed (`python3 scripts/check_version_consistency.py`) + +--- + +## 变更摘要(中文) + +简要描述本次 PR 的改动内容。 + +### 新功能 +- 功能1描述 +- 功能2描述 + +### 问题修复 +- 修复1描述 +HEREDOC +``` + +**Critical rules for the body file:** +- Use `<< 'HEREDOC'` (quoted heredoc) to prevent variable expansion +- Keep all backticks literal — they are safe inside a heredoc +- Paths like `/api/v1/files/` are safe too since heredoc doesn't interpret them as commands + +### Step 4 — Validate PR Body + +Before submitting, verify the body file contains expected sections: + +```bash +# Check key sections exist +grep -q "## Summary" .temp/pr_body.md && echo "✅ Summary" || echo "❌ Summary missing" +grep -q "## Changes" .temp/pr_body.md && echo "✅ Changes" || echo "❌ Changes missing" +grep -q "## 变更摘要" .temp/pr_body.md && echo "✅ CN Section" || echo "❌ CN Section missing" + +# Preview the body +cat .temp/pr_body.md +``` + +Ask the user to confirm the body content before proceeding. + +### Step 5 — Push Branch + +```bash +git push -u origin $(git branch --show-current) +``` + +If push is rejected (non-fast-forward), report to user and ask whether to force-push. **Do NOT force-push without explicit confirmation.** + +### Step 6 — Create Pull Request + +```bash +gh pr create \ + --base main \ + --head $(git branch --show-current) \ + --title "<PR title from Step 2>" \ + --body-file .temp/pr_body.md +``` + +Always use `--body-file`, never `--body` with inline markdown. + +### Step 7 — Verify PR Creation + +```bash +PAGER=cat GH_PAGER=cat gh pr view --json number,url,title,body --jq '{number: .number, url: .url, title: .title, body_preview: .body[:200]}' +``` + +Confirm: +- PR number and URL +- Title matches intended Conventional Commits format +- Body preview includes key sections (not truncated/corrupted) + +If the body appears corrupted (empty sections, missing backtick content), use edit: + +```bash +gh pr edit <PR-NUMBER> --body-file /tmp/pr_body.md +``` + +### Step 8 — Cleanup + +```bash +rm -f .temp/pr_body.md +``` + +**Note**: The `.temp/` directory itself is preserved for reuse; only the individual PR body file is deleted. To fully clean up: `rm -rf .temp/` + +Report final PR URL to the user. + +--- + +## Shell-Escape Safety Rules + +| Risk | Safe Approach | +|------|--------------| +| Backticks in `--body` | Write to file, use `--body-file` | +| Paths like `/api/...` | Safe in heredoc; risky in inline `--body` | +| Newlines in `--body` | File-based only | +| `$variable` expansion | Use `<< 'HEREDOC'` (quoted) | +| Double quotes in body | Safe in heredoc file | +| Temp file storage | Use `.temp/` dir, not `/tmp` | +| Cleanup after use | Always delete temp file (keep dir) | + +--- + +## Anti-Patterns to Avoid + +- ❌ Never use `--body "..."` with multi-line content directly in shell command +- ❌ Never interpolate variables directly into heredoc without quoting the delimiter +- ❌ Never force-push (`--force`) without explicit user confirmation +- ❌ Never target `main` as the source branch (only as base) +- ❌ Never skip the body validation step — a PR with empty body is worse than a delayed PR diff --git a/.agent/skills/release-finalizer/SKILL.md b/.agent/skills/release-finalizer/SKILL.md new file mode 100644 index 0000000..f6e4f1f --- /dev/null +++ b/.agent/skills/release-finalizer/SKILL.md @@ -0,0 +1,208 @@ +--- +name: release-finalizer +description: Merges a release PR, associates it with resolved issues, replies to issue reporters, and closes issues. Use after PR review is complete and ready for merge. Closes the release cycle. +--- + +# Release Finalizer + +## Overview + +This skill completes the final step of the release cycle: merging the release PR to `main`, replying to all related issues with solutions, and automatically closing them using GitHub's issue linking mechanism. + +## Prerequisites + +- The PR is in `OPEN` state and ready to merge +- All status checks have passed (CI green) +- All review feedback has been addressed +- The PR relates to one or more GitHub issues (either in PR description or through commits) + +--- + +## Workflow + +### Step 1 — Pre-Merge Verification + +Verify that the PR is ready: + +```bash +PAGER=cat GH_PAGER=cat gh pr view <PR-NUMBER> --json state,statusCheckRollup,reviewDecision +``` + +Checklist: +- ✅ `state` is `OPEN` +- ✅ `statusCheckRollup` all have `conclusion: SUCCESS` +- ✅ `reviewDecision` is `APPROVED` or empty (no blocking reviews) + +If any check fails, **do NOT merge**. Report the issue to the user. + +### Step 2 — Identify Related Issues + +Issues can be linked to a PR in multiple ways. Check the PR description and commit messages for keywords: + +```bash +PAGER=cat GH_PAGER=cat gh pr view <PR-NUMBER> --json body,commits +``` + +Look for patterns like: +- `Closes #XX`, `Fixes #XX`, `Resolves #XX` (in description or commit bodies) +- `#XX` mentioned as "related to" or "addresses" + +**Manual input**: If issue links are not in the PR, ask the user which issue(s) this PR resolves. + +Extract all issue numbers into a list: `[#48, #52, ...]` + +### Step 3 — Select Merge Strategy + +Offer the user three options: + +| Strategy | Git Behavior | Use Case | +|----------|-------------|----------| +| **Squash** | All commits squashed into one commit on main | Clean history, recommended for release PRs | +| **Rebase** | Linear history, no merge commit | Preserve commit granularity | +| **Merge** | Merge commit created | Preserve full PR context | + +**Recommendation for release PRs**: Use `--squash` to create a single clean commit. + +If user doesn't specify, default to `--squash`. + +### Step 4 — Prepare Merge Commit Message + +If using `--squash`, craft a single comprehensive commit message: + +**Format** (Conventional Commits + Github linking): +``` +type(scope): description + +- Bullet point 1 +- Bullet point 2 + +Closes #48 +Closes #52 +``` + +The `Closes #XX` keyword tells GitHub to automatically close those issues when the commit lands on `main`. + +Example: +``` +feat(pipes,filters): release Copilot SDK Pipe v0.8.0 and Files Filter v0.1.3 + +- Implement P1~P4 conditional tool filtering system +- Fix file publishing reliability across all storage backends +- Add strict file URL validation +- Update bilingual documentation + +Closes #48 +``` + +### Step 5 — Execute Merge + +```bash +gh pr merge <PR-NUMBER> \ + --squash \ + --delete-branch \ + -m "type(scope): description" \ + -b "- Bullet 1\n- Bullet 2\n\nCloses #48" +``` + +**Key flags:** +- `--squash`: Squash commits (recommended for releases) +- `--delete-branch`: Delete the feature branch after merge +- `-m`: Commit subject +- `-b`: Commit body (supports `\n` for newlines) + +Confirm the merge is successful; GitHub will automatically close related issues with `Closes #XX` keyword. + +### Step 6 — Verify Auto-Close + +GitHub automatically closes issues when a commit with `Closes #XX` lands on the default branch (`main`). + +To verify: +```bash +PAGER=cat GH_PAGER=cat gh issue view <ISSUE-NUMBER> --json state +``` + +Should show `state: CLOSED`. + +### Step 7 — Post Closing Message (Optional but Recommended) + +For better UX, manually post a summary comment to **each issue** before it auto-closes (since auto-close happens silently): + +```bash +gh issue comment <ISSUE-NUMBER> --body " +This has been fixed in PR #<PR-NUMBER>, which is now merged to main. + +**Solution Summary:** +- <Key fix 1> +- <Key fix 2> + +The fix will be available in the next plugin release. Thank you for reporting! ⭐ +" +``` + +### Step 8 — (Optional) Regenerate Release Notes + +If the merge revealed any final tweaks to release notes: + +```bash +# Re-export release notes from merged commit +git log --oneline -1 <merged-commit-sha> +``` + +If needed, create a follow-up PR with doc polish (do NOT force-push the merged commit). + +--- + +## Merge Strategy Decision Tree + +``` +Is this a patch/hotfix release? +├─ YES → Use --squash +└─ NO → Multi-feature release? + ├─ YES → Use --squash (cleaner history) + └─ NO → Preserve detail? + ├─ YES → Use --rebase + └─ NO → Use --merge (preserve PR context) +``` + +--- + +## Issue Auto-Close Keywords + +These keywords in commit/PR messages will auto-close issues when merged to `main`: + +- `Closes #XX` +- `Fixes #XX` +- `Resolves #XX` +- `close #XX` (case-insensitive) +- `fix #XX` +- `resolve #XX` + +**Important**: The keyword must be on the **final commit that lands on** `main`. For squash merges, it must be in the squash commit message body. + +--- + +## Anti-Patterns to Avoid + +- ❌ Do NOT merge if any status checks are PENDING or FAILED +- ❌ Do NOT merge if there are blocking reviews (reviewDecision: `CHANGES_REQUESTED`) +- ❌ Do NOT merge without verifying the Conventional Commits format in the merge message +- ❌ Do NOT merge without including `Closes #XX` keywords for all related issues +- ❌ Do NOT assume issues will auto-close silently — post a courtesy comment first +- ❌ Do NOT delete the branch if it might be needed for cherry-pick or hotfixes later + +--- + +## Troubleshooting + +### Issue did not auto-close after merge +- Verify the `Closes #XX` keyword is in the **final commit message** (use `git log` to check) +- Ensure the commit is on the `main` branch +- GitHub sometimes takes a few seconds to process; refresh the issue page + +### Multiple issues to close +- List all in separate `Closes #XX` lines in the commit body +- Each one will be independently auto-closed + +### Want to close issue without merge? +- Use `gh issue close <ISSUE-NUMBER>` manually +- Only recommended if the PR was manually reverted or deemed invalid diff --git a/.agent/skills/release-prep/SKILL.md b/.agent/skills/release-prep/SKILL.md new file mode 100644 index 0000000..91d6da8 --- /dev/null +++ b/.agent/skills/release-prep/SKILL.md @@ -0,0 +1,157 @@ +--- +name: release-prep +description: Orchestrates the full release preparation flow for a plugin — version sync across 7+ files, bilingual release notes creation, and commit message drafting. Use before submitting a PR. Does NOT push or create a PR; that is handled by pr-submitter. +--- + +# Release Prep + +## Overview + +This skill drives the complete pre-PR release pipeline. It enforces the repository rule that every release must synchronize the version number and changelog across **at least 7 locations** before a commit is created. + +## Scope + +This skill covers: +1. Version sync (delegates detail to `version-bumper` if needed) +2. Bilingual release notes file creation +3. 7-location consistency verification +4. Conventional Commits message drafting +5. `git add -A && git commit` execution + +It **stops before** `git push` or `gh pr create`. Use the `pr-submitter` skill for those steps. + +### Temporary File Convention + +Any temporary files created during release prep (e.g., draft changelogs) must: +- Be written to the project's `.temp/` directory, **NOT** system `/tmp` +- Be cleaned up before commit using `rm -f .temp/file_name` +- Never be committed to git (add `.temp/` to `.gitignore`) + +--- + +## Workflow + +### Step 1 — Collect Release Info + +Ask the user (or infer from current state) the following: +- **Plugin name** and **type** (actions / filters / pipes / tools) +- **New version number** (e.g., `0.8.0`) +- **Key changes** in English and Chinese (1-5 bullet points each) + +If a `What's New` section already exists in README.md, extract it as the source of truth. + +### Step 2 — Sync Version Across 7 Locations + +Verify AND update the version string in all of the following. Mark each as ✅ or ❌: + +| # | File | Location | +|---|------|----------| +| 1 | `plugins/{type}/{name}/{name}.py` | `version:` in docstring | +| 2 | `plugins/{type}/{name}/README.md` | `**Version:** x.x.x` metadata line | +| 3 | `plugins/{type}/{name}/README_CN.md` | `**Version:** x.x.x` metadata line | +| 4 | `docs/plugins/{type}/{name}.md` | `**Version:** x.x.x` metadata line | +| 5 | `docs/plugins/{type}/{name}.zh.md` | `**Version:** x.x.x` metadata line | +| 6 | `docs/plugins/{type}/index.md` | version badge for this plugin | +| 7 | `docs/plugins/{type}/index.zh.md` | version badge for this plugin | + +Additionally update the root-level **updated date badge** in: +- `README.md` — `![updated](https://img.shields.io/badge/YYYY--MM--DD-gray?style=flat)` +- `README_CN.md` — same badge format + +Use today's date (`YYYY-MM-DD`) for the badge. + +### Step 3 — Update What's New (All 4 Doc Files) + +The `What's New` / `最新更新` section must contain **only the most recent release's changes**. Previous entries should be removed from this section (they live in CHANGELOG or release notes files). + +Update these 4 files' `What's New` / `最新更新` block consistently: +- `plugins/{type}/{name}/README.md` +- `plugins/{type}/{name}/README_CN.md` +- `docs/plugins/{type}/{name}.md` +- `docs/plugins/{type}/{name}.zh.md` + +### Step 4 — Create Bilingual Release Notes Files + +Create two versioned release notes files: + +**Path**: `plugins/{type}/{name}/v{version}.md` +**Path**: `plugins/{type}/{name}/v{version}_CN.md` + +#### Required Sections + +Each file must include: +1. **Title**: `# v{version} Release Notes` (EN) / `# v{version} 版本发布说明` (CN) +2. **Overview**: One paragraph summarizing this release +3. **New Features** / **新功能**: Bulleted list of features +4. **Bug Fixes** / **问题修复**: Bulleted list of fixes +5. **Migration Notes** / **迁移说明**: Breaking changes or Valve key renames (omit section if none) +6. **Companion Plugins** / **配套插件** (optional): If a companion plugin was updated + +If a release notes file already exists for this version, update it rather than creating a new one. + +#### Full Coverage Rule (Mandatory) + +Release notes must cover **all updates in the current release scope** and not only headline features. + +Minimum required coverage in both EN/CN files: +- New features and capability enhancements +- Bug fixes and reliability fixes +- Documentation/README/doc-mirror updates that affect user understanding or usage +- Terminology/i18n/wording fixes that change visible behavior or messaging + +Before commit, cross-check release notes against `git diff` and ensure no meaningful update is omitted. + +### Step 5 — Verify Consistency (Pre-Commit Check) + +Run the consistency check script: + +```bash +python3 scripts/check_version_consistency.py +``` + +If issues are found, fix them before proceeding. Do not commit with inconsistencies. + +### Step 6 — Draft Conventional Commits Message + +Generate the commit message following `commit-message.instructions.md` rules: +- **Language**: English ONLY +- **Format**: `type(scope): subject` + blank line + body bullets +- **Scope**: use plugin folder name (e.g., `github-copilot-sdk`) +- **Body**: 1-3 bullets summarizing key changes +- Explicitly mention "READMEs and docs synced" if version was bumped + +Present the full commit message to the user for review before executing. + +### Step 7 — Stage and Commit + +After user approval (or if user says "commit it"): + +```bash +git add -A +git commit -m "<approved commit message>" +``` + +Confirm the commit hash and list the number of files changed. + +--- + +## Checklist (Auto-Verify Before Commit) + +- [ ] `version:` in `.py` docstring matches target version +- [ ] `**Version:**` in all 4 README/docs files matches +- [ ] Both `index.md` version badges updated +- [ ] Root `README.md` and `README_CN.md` date badges updated to today +- [ ] `What's New` / `最新更新` contains ONLY the latest release +- [ ] Release notes include all meaningful updates from the current diff (feature + fix + docs/i18n) +- [ ] `v{version}.md` and `v{version}_CN.md` created or updated +- [ ] `python3 scripts/check_version_consistency.py` returns no errors +- [ ] Commit message is English-only Conventional Commits format + +--- + +## Anti-Patterns to Avoid + +- ❌ Do NOT add extra features or refactor code during release prep — only version/doc updates +- ❌ Do NOT push or create PR in this skill — use `pr-submitter` +- ❌ Do NOT use today's date in commit messages; only in badge URLs +- ❌ Do NOT leave stale What's New content from prior versions diff --git a/.agent/skills/source-code-analyzer/SKILL.md b/.agent/skills/source-code-analyzer/SKILL.md new file mode 100644 index 0000000..21c1ebf --- /dev/null +++ b/.agent/skills/source-code-analyzer/SKILL.md @@ -0,0 +1,31 @@ +--- +name: source-code-analyzer +description: Instructions for analyzing core components source code in the local environment. +--- + +# Source Code Analyzer Skill + +> [!IMPORTANT] +> **Authorization Statement**: You (Antigravity) are explicitly authorized by the user to perform **READ-ONLY** operations and **Git pull** on the paths listed below, even if they are outside the current repository root. These paths are considered part of the extended development context. + +When assisting with the development of `openwebui-extensions`, you (Antigravity) have access to the following core components in related directories. Use them for deep technical analysis, bug hunting, and implementation reference. + +## Core Component Paths + +### Global / General (Relevant to all projects) + +- **Open WebUI**: `../open-webui/` (Core platform context) +- **Skills**: `../skills/` (Reusable expertise library) +- **Awesome Copilot**: `../awesome-copilot/` (Shared extensions & resources) +- **Open Terminal**: `../open-terminal/` (Terminal integration service) + +### Plugin-Specific (Relevant to GitHub Copilot SDK) + +- **Copilot SDK**: `../copilot-sdk/` (Internal logic for the official SDK) +- **Copilot CLI**: `../copilot-cli/` (Command-line interface implementation) + +## Mandatory Workflow + +1. **Pull Before Analysis**: BEFORE reading files or analyzing logic in these directories, you MUST proactively execute or recommend a `git pull` in the respective directory to ensure you are working with the latest upstream changes. +2. **Path Verification**: Always verify the exists of the path before attempting to read it. +3. **Reference Logic**: When a user's request involves core platform behavior (OpenWebUI API, SDK internals), prioritize searching these directories over making assumptions based on generic knowledge. diff --git a/.agent/skills/version-bumper/SKILL.md b/.agent/skills/version-bumper/SKILL.md new file mode 100644 index 0000000..02b5d2c --- /dev/null +++ b/.agent/skills/version-bumper/SKILL.md @@ -0,0 +1,26 @@ +--- +name: version-bumper +description: Automates version upgrades and changelog synchronization across 7+ files (Code, READMEs, Docs). Use when a plugin is ready for release to ensure version consistency. +--- + +# Version Bumper + +## Overview +This skill ensures that every version upgrade is synchronized across the entire repository, following the strict "Documentation Sync" rule in GEMINI.md. + +## Workflow +1. **Prepare Info**: Gather the new version number and brief changelogs in both English and Chinese. +2. **Auto-Patch**: The skill will help you identify and update: + - `plugins/.../name.py` (docstring version) + - `plugins/.../README.md` (metadata & What's New) + - `plugins/.../README_CN.md` (metadata & 最新更新) + - `docs/plugins/...md` (mirrors) + - `docs/plugins/index.md` (version badge) + - `README.md` (updated date badge) +3. **Verify**: Check the diffs to ensure no formatting was broken. + +## Tool Integration +Execute the bump script (draft): +```bash +python3 scripts/bump.py <version> "<message_en>" "<message_zh>" +``` diff --git a/.agent/skills/version-bumper/scripts/bump.py b/.agent/skills/version-bumper/scripts/bump.py new file mode 100644 index 0000000..c2c9ffe --- /dev/null +++ b/.agent/skills/version-bumper/scripts/bump.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +import sys +import os +import re +from datetime import datetime + +def patch_file(file_path, old_pattern, new_content, is_regex=False): + if not os.path.exists(file_path): + print(f"Warning: File not found: {file_path}") + return False + + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + if is_regex: + new_content_result = re.sub(old_pattern, new_content, content, flags=re.MULTILINE) + else: + new_content_result = content.replace(old_pattern, new_content) + + if new_content_result != content: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(new_content_result) + print(f"✅ Patched: {file_path}") + return True + else: + print(f"ℹ️ No change needed: {file_path}") + return False + +def bump_version(plugin_type, plugin_name, new_version, msg_en, msg_zh): + print(f"🚀 Bumping {plugin_name} ({plugin_type}) to {new_version}...") + + today = datetime.now().strftime("%Y-%m-%d") + today_badge = today.replace("-", "--") + + # 1. Patch Plugin Python File + py_file = f"plugins/{plugin_type}/{plugin_name}/{plugin_name}.py" + patch_file(py_file, r"version: \d+\.\d+\.\d+", f"version: {new_version}", is_regex=True) + + # 2. Patch Plugin READMEs + readme_en = f"plugins/{plugin_type}/{plugin_name}/README.md" + readme_zh = f"plugins/{plugin_type}/{plugin_name}/README_CN.md" + + # Update version in metadata + patch_file(readme_en, r"\*\*Version:\*\* \d+\.\d+\.\d+", f"**Version:** {new_version}", is_regex=True) + patch_file(readme_zh, r"\*\*版本:\*\* \d+\.\d+\.\d+", f"**版本:** {new_version}", is_regex=True) + + # Update What's New (Assuming standard headers) + patch_file(readme_en, r"## 🔥 What's New in v.*?\n", f"## 🔥 What's New in v{new_version}\n\n* {msg_en}\n", is_regex=True) + patch_file(readme_zh, r"## 🔥 最新更新 v.*?\n", f"## 🔥 最新更新 v{new_version}\n\n* {msg_zh}\n", is_regex=True) + + # 3. Patch Docs Mirrors + doc_en = f"docs/plugins/{plugin_type}/{plugin_name}.md" + doc_zh = f"docs/plugins/{plugin_type}/{plugin_name}.zh.md" + patch_file(doc_en, r"\*\*Version:\*\* \d+\.\d+\.\d+", f"**Version:** {new_version}", is_regex=True) + patch_file(doc_zh, r"\*\*版本:\*\* \d+\.\d+\.\d+", f"**版本:** {new_version}", is_regex=True) + + # 4. Patch Root READMEs (Updated Date Badge) + patch_file("README.md", r"badge/202\d--\d\d--\d\d-gray", f"badge/{today_badge}-gray", is_regex=True) + patch_file("README_CN.md", r"badge/202\d--\d\d--\d\d-gray", f"badge/{today_badge}-gray", is_regex=True) + + print("\n✨ All synchronization tasks completed.") + return True + +if __name__ == "__main__": + if len(sys.argv) < 6: + print("Usage: bump.py <type> <name> <version> <msg_en> <msg_zh>") + print("Example: bump.py filters markdown_normalizer 1.2.8 'Fix bug' '修复错误'") + sys.exit(1) + + bump_version(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]) diff --git a/README.md b/README.md index 1a43b55..049cc99 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ A collection of enhancements, plugins, and prompts for [open-webui](https://gith | Rank | Plugin | Version | Downloads | Views | 📅 Updated | | :---: | :--- | :---: | :---: | :---: | :---: | -| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![p1_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_version.json&style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--28-gray?style=flat) | +| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![p1_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_version.json&style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--04-gray?style=flat) | | 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | ![p2_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_version.json&style=flat) | ![p2_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_dl.json&style=flat) | ![p2_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--13-gray?style=flat) | | 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![p3_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_version.json&style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--28-gray?style=flat) | | 4️⃣ | [Export to Word Enhanced](https://openwebui.com/posts/export_to_word_enhanced_formatting_fca6a315) | ![p4_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_version.json&style=flat) | ![p4_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_dl.json&style=flat) | ![p4_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p4_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--13-gray?style=flat) | @@ -104,6 +104,11 @@ Located in the `plugins/` directory, containing Python-based enhancements: - **Export to Excel** (`export_to_excel`): Exports chat history to Excel files. - **Export to Word** (`export_to_docx`): Exports chat history to Word documents. +### Tools + +- **Smart Mind Map Tool** (`smart-mind-map-tool`): The tool version of Smart Mind Map, enabling AI proactive/autonomous invocation. +- **OpenWebUI Skills Manager Tool** (`openwebui-skills-manager-tool`): Native tool for managing OpenWebUI skills. + ### Filters - **GitHub Copilot SDK Files Filter** (`github_copilot_sdk_files_filter`): Essential companion for Copilot SDK. Bypasses RAG to ensure full file accessibility for Agents. diff --git a/README_CN.md b/README_CN.md index 54bad5f..7b5e24c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -21,7 +21,7 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词 | 排名 | 插件 | 版本 | 下载 | 浏览 | 📅 更新 | | :---: | :--- | :---: | :---: | :---: | :---: | -| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![p1_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_version.json&style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--28-gray?style=flat) | +| 🥇 | [Smart Mind Map](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) | ![p1_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_version.json&style=flat) | ![p1_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_dl.json&style=flat) | ![p1_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p1_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--03--04-gray?style=flat) | | 🥈 | [Smart Infographic](https://openwebui.com/posts/smart_infographic_ad6f0c7f) | ![p2_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_version.json&style=flat) | ![p2_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_dl.json&style=flat) | ![p2_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p2_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--13-gray?style=flat) | | 🆕 | [GitHub Copilot Official SDK Pipe](https://openwebui.com/posts/github_copilot_official_sdk_pipe_ce96f7b4) | ![p0_version](https://img.shields.io/badge/版本-0.9.1-blue?style=flat) | ![p0_dl](https://img.shields.io/badge/下载-热门-red?style=flat) | ![p0_vw](https://img.shields.io/badge/浏览-最新-green?style=flat) | ![updated](https://img.shields.io/badge/2026--03--04-gray?style=flat) | | 🥉 | [Markdown Normalizer](https://openwebui.com/posts/markdown_normalizer_baaa8732) | ![p3_version](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_version.json&style=flat) | ![p3_dl](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_dl.json&style=flat) | ![p3_vw](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FFu-Jie%2Fdb3d95687075a880af6f1fba76d679c6%2Fraw%2Fbadge_p3_vw.json&style=flat) | ![updated](https://img.shields.io/badge/2026--02--28-gray?style=flat) | @@ -102,6 +102,11 @@ OpenWebUI 增强功能集合。包含个人开发与收集的插件、提示词 - **Export to Excel** (`export_to_excel`): 将对话内容导出为 Excel 文件。 - **Export to Word** (`export_to_docx`): 将对话内容导出为 Word 文档。 +### Tools (工具) + +- **智能思维导图工具** (`smart-mind-map-tool`): 思维导图的 Tool 版本,支持 AI 主动/自主调用。 +- **OpenWebUI Skills 管理工具** (`openwebui-skills-manager-tool`): 用于管理 OpenWebUI Skills 的原生工具。 + ### Filters (消息处理) - **GitHub Copilot SDK Files Filter** (`github_copilot_sdk_files_filter`): Copilot SDK 必备搭档。绕过 RAG,确保 Agent 能真正看到你的每一个文件。 diff --git a/docs/plugins/tools/index.md b/docs/plugins/tools/index.md index 2e01e47..775a5da 100644 --- a/docs/plugins/tools/index.md +++ b/docs/plugins/tools/index.md @@ -5,3 +5,4 @@ OpenWebUI native Tool plugins that can be used across models. ## Available Tool Plugins - [OpenWebUI Skills Manager Tool](openwebui-skills-manager-tool.md) (v0.2.1) - Simple native skill management (`list/show/install/create/update/delete`). +- [Smart Mind Map Tool](smart-mind-map-tool.md) (v1.0.0) - Intelligently analyzes text content and proactively generates interactive mind maps to help users structure and visualize knowledge. diff --git a/docs/plugins/tools/index.zh.md b/docs/plugins/tools/index.zh.md index 9a35cd8..f0d8e34 100644 --- a/docs/plugins/tools/index.zh.md +++ b/docs/plugins/tools/index.zh.md @@ -5,3 +5,4 @@ ## 可用 Tool 插件 - [OpenWebUI Skills 管理工具](openwebui-skills-manager-tool.zh.md) (v0.2.1) - 简化技能管理(`list/show/install/create/update/delete`)。 +- [智能思维导图工具 (Smart Mind Map Tool)](smart-mind-map-tool.zh.md) (v1.0.0) - 智能分析文本内容并主动生成交互式思维导图,帮助用户结构化与可视化知识。 diff --git a/docs/plugins/tools/smart-mind-map-tool.md b/docs/plugins/tools/smart-mind-map-tool.md new file mode 100644 index 0000000..b34a491 --- /dev/null +++ b/docs/plugins/tools/smart-mind-map-tool.md @@ -0,0 +1,62 @@ +# Smart Mind Map Tool - Knowledge Visualization & Structuring + +Smart Mind Map Tool is the tool version of the popular Smart Mind Map plugin for OpenWebUI. It allows the model to proactively generate interactive mind maps during conversations by intelligently analyzing context and structuring knowledge into visual hierarchies. + +> ℹ️ **Note**: Prefer the manual trigger button instead? Check out the [Smart Mind Map Action Version](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) here. + +**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.0.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT + +--- + +## 🚀 Why is there a Tool version? + +1. **Powered by OpenWebUI 0.8.0 Rich UI**: Previous versions of OpenWebUI did not support embedding custom HTML/iframes directly into the chat stream. Starting with 0.8.0, the platform introduced full Rich UI rendering support for **both Actions and Tools**, unleashing interactive frontend possibilities. +2. **AI Autonomous Invocation (vs. Action)**: While an **Action** is passive and requires a manual button click from the user, the **Tool** version gives the model **autonomy**. The AI can analyze the conversational context and decide on its own exactly when generating a mind map would be most helpful, offering a true "smart assistant" experience. + +It is perfect for: + +- Summarizing complex discussions. +- Planning projects or outlining articles. +- Explaining hierarchical concepts. + +## ✨ Key Features + +- ✅ **Proactive Generation**: The AI triggers the tool automatically when it senses a need for structured visualization. +- ✅ **Full Context Awareness**: Supports aggregation of the entire conversation history to generate comprehensive knowledge maps. +- ✅ **Native Multi-language UI (i18n)**: Automatically detects and adapts to your browser/system language (en-US, zh-CN, ja-JP, etc.). +- ✅ **Premium UI/UX**: Matches the Action version with a compact toolbar, glassmorphism aesthetics, and professional borders. +- ✅ **Interactive Controls**: Zoom (In/Out/Reset), Level-based expansion (Default to Level 3), and Fullscreen mode. +- ✅ **High-Quality Export**: Export your mind maps as print-ready PNG images. + +## 🛠️ Installation & Setup + +1. **Install**: Upload `smart_mind_map_tool.py` to your OpenWebUI Admin Settings -> Plugins -> Tools. +2. **Enable Native Tool Calling**: Navigate to `Admin Settings -> Models` or your workspace settings, and ensure that **Native Tool Calling** is enabled for your selected model. This is required for the AI to reliably and actively invoke the tool automatically. +3. **Assign**: Toggle the tool "ON" for your desired models in the workspace or model settings. +4. **Configure**: + - `MESSAGE_COUNT`: Set to `12` (default) to use the 12 most recent messages, or `0` for the entire conversation history. + - `MODEL_ID`: Specify a preferred model for analysis (defaults to the current chat model). + +## ⚙️ Configuration (Valves) + +| Parameter | Default | Description | +| :--- | :--- | :--- | +| `MODEL_ID` | (Empty) | The model used for text analysis. If empty, uses the current chat model. | +| `MESSAGE_COUNT` | `12` | Number of messages to aggregate. `0` = All messages. | +| `MIN_TEXT_LENGTH` | `100` | Minimum character count required to trigger a mind map. | + +## ❓ FAQ & Troubleshooting + +- **Language mismatch?**: The tool uses a 4-level detection (Frontend Script > Browser Header > User Profile > Default). Ensure your browser language is set correctly. +- **Too tiny or too large?**: We've optimized the height to `500px` for inline chat display with a responsive "Fit to Screen" logic. +- **Exporting**: Click the "⛶" for fullscreen if you want a wider view before exporting to PNG. + +--- + +## ⭐ Support + +If this tool helps you visualize ideas better, please give us a star on [GitHub](https://github.com/Fu-Jie/openwebui-extensions). + +## ⚖️ License + +MIT License. Developed with ❤️ by Fu-Jie. diff --git a/docs/plugins/tools/smart-mind-map-tool.zh.md b/docs/plugins/tools/smart-mind-map-tool.zh.md new file mode 100644 index 0000000..3a6c47a --- /dev/null +++ b/docs/plugins/tools/smart-mind-map-tool.zh.md @@ -0,0 +1,62 @@ +# 思维导图工具 - 知识可视化与结构化利器 + +思维导图工具(Smart Mind Map Tool)是广受好评的“思维导图”插件的工具(Tool)版本。它赋予了模型主动生成交互式思维导图的能力,通过智能分析上下文,将碎片化知识转化为层级分明的视觉架构。 + +> ℹ️ **说明**:如果您更倾向于手动点击按钮触发生成,可以获取 [思维导图 Action(动作)版本](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a)。 + +**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 1.0.0 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT + +--- + +## 🚀 为什么会有工具(Tool)版本? + +1. **得益于 OpenWebUI 0.8.0 的 Rich UI 特性**:在以前的版本中,是不支持直接将自定义的 HTML/iframe 嵌入到对话流中的。而从 0.8.0 开始,平台不仅支持了这种顺滑的前端组件直出(Rich UI),而且同时对 **Action** 和 **Tool** 开放了该能力。 +2. **AI 自主调用(区别于 Action)**:**Action** 是被动的,需要用户在输入框或消息旁手动点击触发;而 **Tool** 赋予了模型**自主权**。AI 可以根据对话上下文,自行判断在什么时候为您生成导图最有帮助,实现真正的“智能助理”体验。 + +它非常适合以下场景: + +- 总结复杂的对话内容。 +- 规划项目、整理文章大纲。 +- 解释具有层级结构的抽象概念。 + +## ✨ 核心特性 + +- ✅ **主动触发生成**:AI 在感知到需要视觉化展示时会自动调用工具生成导图。 +- ✅ **全量上下文感知**:支持聚合整个会话历史(MESSAGE_COUNT 为 0),生成最完整的知识地图。 +- ✅ **原生多语言 UI (i18n)**:自动检测并适配浏览器/系统语言(简体中文、繁体中文、英文、日文、韩文等)。 +- ✅ **统一的高级视觉**:完全复刻 Action 版本的极简工具栏、玻璃拟态审美以及专业边框阴影。 +- ✅ **深度交互控制**:支持缩放(放大/缩小/重置)、层级调节(默认为 3 级展开)以及全屏模式。 +- ✅ **高品质导出**:支持将导图导出为超高清 PNG 图片。 + +## 🛠️ 安装与设置 + +1. **安装**:在 OpenWebUI 管理员设置 -> 插件 -> 工具中上传 `smart_mind_map_tool.py`。 +2. **启用原生理机制**:在“管理员设置 -> 模型”或配置里,确保目标模型**启用了原生工具调用(Native Tool Calling)**。只有开启这个能力,AI 才能自主并稳定地触发 Tool 功能。 +3. **分配工具**:在工作区或聊天界面处为目标模型选中并挂载本工具。 +4. **配置**: + - `MESSAGE_COUNT`:设置为 `12`(默认)以使用最近的 12 条对话记录,或设置为 `0` 聚合全部历史。 + - `MODEL_ID`:指定分析导图时偏好的模型(留空则默认使用当前模型)。 + +## ⚙️ 配置参数 (Valves) + +| 参数 | 默认值 | 描述 | +| :--- | :--- | :--- | +| `MODEL_ID` | (留空) | 用于文本分析的模型 ID。留空则随当前聊天模型。 | +| `MESSAGE_COUNT` | `12` | 聚合消息的数量。`0` 表示全量消息,`12` 表示截取最近的 12 条。 | +| `MIN_TEXT_LENGTH` | `100` | 触发导图分析所需的最小字符长度。 | + +## ❓ 常见问题 + +- **语言显示不正确?**:工具采用 4 级探测机制(前端脚本 > 浏览器头 > 用户资料 > 默认)。请检查浏览器语言设置。 +- **生成的导图太小或太大?**:我们针对对话流内联显示优化了 `500px` 的固定高度,并配有自适应缩放逻辑。 +- **导出图片**:建议先点击“⛶”进入全屏,获得最佳构图后再点击导出。 + +--- + +## ⭐ 支持 + +如果这个工具帮您理清了思路,欢迎在 [GitHub](https://github.com/Fu-Jie/openwebui-extensions) 给我们一个 Star。 + +## ⚖️ 许可证 + +MIT License. Designed with ❤️ by Fu-Jie. diff --git a/plugins/filters/github_copilot_sdk_files_filter/github_copilot_sdk_files_filter_cn.py b/plugins/filters/github_copilot_sdk_files_filter/github_copilot_sdk_files_filter_cn.py index 085809e..b20bf36 100644 --- a/plugins/filters/github_copilot_sdk_files_filter/github_copilot_sdk_files_filter_cn.py +++ b/plugins/filters/github_copilot_sdk_files_filter/github_copilot_sdk_files_filter_cn.py @@ -4,7 +4,7 @@ id: github_copilot_sdk_files_filter author: Fu-Jie author_url: https://github.com/Fu-Jie/openwebui-extensions funding_url: https://github.com/open-webui -version: 0.1.2 +version: 0.1.3 description: 一个专门的过滤器,用于绕过 OpenWebUI 默认的 RAG 机制,针对 GitHub Copilot SDK 模型。它将上传的文件移动到安全位置 ('copilot_files'),以便 Copilot Pipe 可以原生处理它们而不受干扰。 """ diff --git a/plugins/tools/smart-mind-map-tool/README.md b/plugins/tools/smart-mind-map-tool/README.md new file mode 100644 index 0000000..bbe7c3a --- /dev/null +++ b/plugins/tools/smart-mind-map-tool/README.md @@ -0,0 +1,62 @@ +# Smart Mind Map Tool - Knowledge Visualization & Structuring + +Smart Mind Map Tool is the tool version of the popular Smart Mind Map action plugin for Open WebUI. It allows the model to proactively generate interactive mind maps during conversations by intelligently analyzing context and structuring knowledge into visual hierarchies. + +> ℹ️ **Note**: Prefer using the manual trigger button instead? Check out the [Smart Mind Map Action Version](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a) here. + +**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 1.0.0 | **Project:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **License:** MIT + +--- + +## Why is there a Tool version? + +1. **Powered by OpenWebUI 0.8.0 Rich UI**: Previous versions of OpenWebUI did not support embedding custom HTML/iframes directly into the chat stream. Starting with 0.8.0, the platform introduced full Rich UI rendering support for **both Actions and Tools**, unleashing interactive frontend possibilities. +2. **AI Autonomous Invocation (vs. Action)**: While an **Action** is passive and requires a manual button click from the user, the **Tool** version gives the model **autonomy**. The AI can analyze the conversational context and decide on its own exactly when generating a mind map would be most helpful, offering a true "smart assistant" experience. + +It is perfect for: + +- Summarizing complex discussions. +- Planning projects or outlining articles. +- Explaining hierarchical concepts. + +## ✨ Key Features + +- ✅ **Proactive Generation**: The AI triggers the tool automatically when it senses a need for structured visualization. +- ✅ **Full Context Awareness**: Supports aggregation of the entire conversation history to generate comprehensive knowledge maps. +- ✅ **Native Multi-language UI (i18n)**: Automatically detects and adapts to your browser/system language (en-US, zh-CN, ja-JP, etc.). +- ✅ **Premium UI/UX**: Matches the Action version with a compact toolbar, glassmorphism aesthetics, and professional borders. +- ✅ **Interactive Controls**: Zoom (In/Out/Reset), Level-based expansion (Default to Level 3), and Fullscreen mode. +- ✅ **High-Quality Export**: Export your mind maps as print-ready PNG images. + +## 🛠️ Installation & Setup + +1. **Install**: Upload `smart_mind_map_tool.py` to your OpenWebUI Admin Settings -> Plugins -> Tools. +2. **Enable Native Tool Calling**: Navigate to `Admin Settings -> Models` or your workspace settings, and ensure that **Native Tool Calling** is enabled for your selected model. This is required for the AI to reliably and actively invoke the tool automatically. +3. **Assign**: Toggle the tool "ON" for your desired models in the workspace or model settings. +4. **Configure**: + - `MESSAGE_COUNT`: Set to `12` (default) to use the 12 most recent messages, or `0` for the entire conversation history. + - `MODEL_ID`: Specify a preferred model for analysis (defaults to the current chat model). + +## ⚙️ Configuration (Valves) + +| Parameter | Default | Description | +| :--- | :--- | :--- | +| `MODEL_ID` | (Empty) | The model used for text analysis. If empty, uses the current chat model. | +| `MESSAGE_COUNT` | `12` | Number of messages to aggregate. `0` = All messages. | +| `MIN_TEXT_LENGTH` | `100` | Minimum character count required to trigger a mind map. | + +## ❓ FAQ & Troubleshooting + +- **Language mismatch?**: The tool uses a 4-level detection (Frontend Script > Browser Header > User Profile > Default). Ensure your browser language is set correctly. +- **Too tiny or too large?**: We've optimized the height to `500px` for inline chat display with a responsive "Fit to Screen" logic. +- **Exporting**: Click the "⛶" for fullscreen if you want a wider view before exporting to PNG. + +--- + +## ⭐ Support + +If this tool helps you visualize ideas better, please give us a star on [GitHub](https://github.com/Fu-Jie/openwebui-extensions). + +## ⚖️ License + +MIT License. Developed with ❤️ by Fu-Jie. diff --git a/plugins/tools/smart-mind-map-tool/README_CN.md b/plugins/tools/smart-mind-map-tool/README_CN.md new file mode 100644 index 0000000..3a6c47a --- /dev/null +++ b/plugins/tools/smart-mind-map-tool/README_CN.md @@ -0,0 +1,62 @@ +# 思维导图工具 - 知识可视化与结构化利器 + +思维导图工具(Smart Mind Map Tool)是广受好评的“思维导图”插件的工具(Tool)版本。它赋予了模型主动生成交互式思维导图的能力,通过智能分析上下文,将碎片化知识转化为层级分明的视觉架构。 + +> ℹ️ **说明**:如果您更倾向于手动点击按钮触发生成,可以获取 [思维导图 Action(动作)版本](https://openwebui.com/posts/turn_any_text_into_beautiful_mind_maps_3094c59a)。 + +**作者:** [Fu-Jie](https://github.com/Fu-Jie) | **版本:** 1.0.0 | **项目:** [OpenWebUI Extensions](https://github.com/Fu-Jie/openwebui-extensions) | **许可证:** MIT + +--- + +## 🚀 为什么会有工具(Tool)版本? + +1. **得益于 OpenWebUI 0.8.0 的 Rich UI 特性**:在以前的版本中,是不支持直接将自定义的 HTML/iframe 嵌入到对话流中的。而从 0.8.0 开始,平台不仅支持了这种顺滑的前端组件直出(Rich UI),而且同时对 **Action** 和 **Tool** 开放了该能力。 +2. **AI 自主调用(区别于 Action)**:**Action** 是被动的,需要用户在输入框或消息旁手动点击触发;而 **Tool** 赋予了模型**自主权**。AI 可以根据对话上下文,自行判断在什么时候为您生成导图最有帮助,实现真正的“智能助理”体验。 + +它非常适合以下场景: + +- 总结复杂的对话内容。 +- 规划项目、整理文章大纲。 +- 解释具有层级结构的抽象概念。 + +## ✨ 核心特性 + +- ✅ **主动触发生成**:AI 在感知到需要视觉化展示时会自动调用工具生成导图。 +- ✅ **全量上下文感知**:支持聚合整个会话历史(MESSAGE_COUNT 为 0),生成最完整的知识地图。 +- ✅ **原生多语言 UI (i18n)**:自动检测并适配浏览器/系统语言(简体中文、繁体中文、英文、日文、韩文等)。 +- ✅ **统一的高级视觉**:完全复刻 Action 版本的极简工具栏、玻璃拟态审美以及专业边框阴影。 +- ✅ **深度交互控制**:支持缩放(放大/缩小/重置)、层级调节(默认为 3 级展开)以及全屏模式。 +- ✅ **高品质导出**:支持将导图导出为超高清 PNG 图片。 + +## 🛠️ 安装与设置 + +1. **安装**:在 OpenWebUI 管理员设置 -> 插件 -> 工具中上传 `smart_mind_map_tool.py`。 +2. **启用原生理机制**:在“管理员设置 -> 模型”或配置里,确保目标模型**启用了原生工具调用(Native Tool Calling)**。只有开启这个能力,AI 才能自主并稳定地触发 Tool 功能。 +3. **分配工具**:在工作区或聊天界面处为目标模型选中并挂载本工具。 +4. **配置**: + - `MESSAGE_COUNT`:设置为 `12`(默认)以使用最近的 12 条对话记录,或设置为 `0` 聚合全部历史。 + - `MODEL_ID`:指定分析导图时偏好的模型(留空则默认使用当前模型)。 + +## ⚙️ 配置参数 (Valves) + +| 参数 | 默认值 | 描述 | +| :--- | :--- | :--- | +| `MODEL_ID` | (留空) | 用于文本分析的模型 ID。留空则随当前聊天模型。 | +| `MESSAGE_COUNT` | `12` | 聚合消息的数量。`0` 表示全量消息,`12` 表示截取最近的 12 条。 | +| `MIN_TEXT_LENGTH` | `100` | 触发导图分析所需的最小字符长度。 | + +## ❓ 常见问题 + +- **语言显示不正确?**:工具采用 4 级探测机制(前端脚本 > 浏览器头 > 用户资料 > 默认)。请检查浏览器语言设置。 +- **生成的导图太小或太大?**:我们针对对话流内联显示优化了 `500px` 的固定高度,并配有自适应缩放逻辑。 +- **导出图片**:建议先点击“⛶”进入全屏,获得最佳构图后再点击导出。 + +--- + +## ⭐ 支持 + +如果这个工具帮您理清了思路,欢迎在 [GitHub](https://github.com/Fu-Jie/openwebui-extensions) 给我们一个 Star。 + +## ⚖️ 许可证 + +MIT License. Designed with ❤️ by Fu-Jie. diff --git a/plugins/tools/smart-mind-map-tool/smart_mind_map_tool.py b/plugins/tools/smart-mind-map-tool/smart_mind_map_tool.py index 1ed6698..1c44147 100644 --- a/plugins/tools/smart-mind-map-tool/smart_mind_map_tool.py +++ b/plugins/tools/smart-mind-map-tool/smart_mind_map_tool.py @@ -3,239 +3,1673 @@ title: Smart Mind Map Tool author: Fu-Jie author_url: https://github.com/Fu-Jie/openwebui-extensions funding_url: https://github.com/open-webui -version: 1.1.0 -description: Intelligently analyzes text content and generates interactive mind maps inline to help users structure and visualize knowledge. +version: 1.0.0 +required_open_webui_version: 0.8.0 +description: Intelligently analyzes text content and generates interactive mind maps to help users structure and visualize knowledge. """ import asyncio import logging +import os import re import time import json from datetime import datetime, timezone from typing import Any, Callable, Awaitable, Dict, Optional +from zoneinfo import ZoneInfo -from fastapi import Request +from fastapi import Request, Response +from fastapi.responses import HTMLResponse from pydantic import BaseModel, Field from open_webui.utils.chat import generate_chat_completion from open_webui.models.users import Users +logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) +TRANSLATIONS = { + "en-US": { + "status_starting": "Smart Mind Map is starting, generating mind map for you...", + "error_no_content": "Unable to retrieve valid user message content.", + "error_text_too_short": "Text content is too short ({len} characters), unable to perform effective analysis. Please provide at least {min_len} characters of text.", + "status_analyzing": "Smart Mind Map: Analyzing text structure in depth...", + "status_drawing": "Smart Mind Map: Drawing completed!", + "notification_success": "Mind map has been generated, {user_name}!", + "error_processing": "Smart Mind Map processing failed: {error}", + "error_user_facing": "Sorry, Smart Mind Map encountered an error during processing: {error}.\nPlease check the Open WebUI backend logs for more details.", + "status_failed": "Smart Mind Map: Processing failed.", + "notification_failed": "Smart Mind Map generation failed, {user_name}!", + "status_rendering_image": "Smart Mind Map: Rendering image...", + "status_image_generated": "Smart Mind Map: Image generated!", + "notification_image_success": "Mind map image has been generated, {user_name}!", + "ui_title": "🧠 Smart Mind Map", + "ui_user": "User:", + "ui_time": "Time:", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "-", + "ui_zoom_reset": "Reset", + "ui_zoom_in": "+", + "ui_depth_select": "Expand Level", + "ui_depth_all": "Expand All", + "ui_depth_2": "Level 2", + "ui_depth_3": "Level 3", + "ui_fullscreen": "Fullscreen", + "ui_theme": "Theme", + "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ Unable to load mind map: Missing valid content.", + "html_error_load_failed": "⚠️ Resource loading failed, please try again later.", + "js_done": "Done", + "js_failed": "Failed", + "js_generating": "Generating...", + "js_filename": "mindmap.png", + "js_upload_failed": "Upload failed: ", + "md_image_alt": "🧠 Mind Map", + "notification_waiting": "Analysis is taking longer than expected. Please wait, we're still working on your mind map...", + }, + "zh-CN": { + "status_starting": "思维导图已启动,正在为您生成思维导图...", + "error_no_content": "无法获取有效的用户消息内容。", + "error_text_too_short": "文本内容过短({len}字符),无法进行有效分析。请提供至少{min_len}字符的文本。", + "status_analyzing": "思维导图:深入分析文本结构...", + "status_drawing": "思维导图:绘制完成!", + "notification_success": "思维导图已生成,{user_name}!", + "error_processing": "思维导图处理失败:{error}", + "error_user_facing": "抱歉,思维导图在处理时遇到错误:{error}。\n请检查Open WebUI后端日志获取更多详情。", + "status_failed": "思维导图:处理失败。", + "notification_failed": "思维导图生成失败,{user_name}!", + "status_rendering_image": "思维导图:正在渲染图片...", + "status_image_generated": "思维导图:图片已生成!", + "notification_image_success": "思维导图图片已生成,{user_name}!", + "ui_title": "🧠 智能思维导图", + "ui_user": "用户:", + "ui_time": "时间:", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "缩小", + "ui_zoom_reset": "重置", + "ui_zoom_in": "放大", + "ui_depth_select": "展开层级", + "ui_depth_all": "全部展开", + "ui_depth_2": "展开 2 级", + "ui_depth_3": "展开 3 级", + "ui_fullscreen": "全屏", + "ui_theme": "主题", + "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ 无法加载思维导图:缺少有效内容。", + "html_error_load_failed": "⚠️ 资源加载失败,请稍后重试。", + "js_done": "完成", + "js_failed": "失败", + "js_generating": "生成中...", + "js_filename": "思维导图.png", + "js_upload_failed": "上传失败:", + "md_image_alt": "🧠 思维导图", + "notification_waiting": "分析时间可能比预期稍长,请稍等,我们正在为您拼命绘图中...", + }, + "zh-HK": { + "status_starting": "思維導圖已啟動,正在為您生成思維導圖...", + "error_no_content": "無法獲取有效的用戶消息內容。", + "error_text_too_short": "文本內容過短({len}字元),無法進行有效分析。請提供至少{min_len}字元的文本。", + "status_analyzing": "思維導圖:深入分析文本結構...", + "status_drawing": "思維導圖:繪製完成!", + "notification_success": "思維導圖已生成,{user_name}!", + "error_processing": "思維導圖處理失敗:{error}", + "error_user_facing": "抱歉,思維導圖在處理時遇到錯誤:{error}。\n請檢查Open WebUI後端日誌獲取更多詳情。", + "status_failed": "思維導圖:處理失敗。", + "notification_failed": "思維導圖生成失敗,{user_name}!", + "status_rendering_image": "思維導圖:正在渲染圖片...", + "status_image_generated": "思維導圖:圖片已生成!", + "notification_image_success": "思維導圖圖片已生成,{user_name}!", + "ui_title": "🧠 智能思維導圖", + "ui_user": "用戶:", + "ui_time": "時間:", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "縮小", + "ui_zoom_reset": "重置", + "ui_zoom_in": "放大", + "ui_depth_select": "展開層級", + "ui_depth_all": "全部展開", + "ui_depth_2": "展開 2 級", + "ui_depth_3": "展開 3 級", + "ui_fullscreen": "全屏", + "ui_theme": "主題", + "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ 無法加載思維導圖:缺少有效內容。", + "html_error_load_failed": "⚠️ 資源加載失敗,請稍後重試。", + "js_done": "完成", + "js_failed": "失敗", + "js_generating": "生成中...", + "js_filename": "思維導圖.png", + "js_upload_failed": "上傳失敗:", + "md_image_alt": "🧠 思維導圖", + "notification_waiting": "分析時間可能比預期稍長,請稍等,我們正在為您拼命繪圖中...", + }, + "zh-TW": { + "status_starting": "思維導圖已啟動,正在為您生成思維導圖...", + "error_no_content": "無法獲取有效的用戶消息內容。", + "error_text_too_short": "文本內容過短({len}字元),無法進行有效分析。請提供至少{min_len}字元的文本。", + "status_analyzing": "思維導圖:深入分析文本結構...", + "status_drawing": "思維導圖:繪製完成!", + "notification_success": "思維導圖已生成,{user_name}!", + "error_processing": "思維導圖處理失敗:{error}", + "error_user_facing": "抱歉,思維導圖在處理時遇到錯誤:{error}。\n請檢查Open WebUI後端日誌獲取更多詳情。", + "status_failed": "思維導圖:處理失敗。", + "notification_failed": "思維導圖生成失敗,{user_name}!", + "status_rendering_image": "思維導圖:正在渲染圖片...", + "status_image_generated": "思維導圖:圖片已生成!", + "notification_image_success": "思維導圖圖片已生成,{user_name}!", + "ui_title": "🧠 智能思維導圖", + "ui_user": "用戶:", + "ui_time": "時間:", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "縮小", + "ui_zoom_reset": "重置", + "ui_zoom_in": "放大", + "ui_depth_select": "展開層級", + "ui_depth_all": "全部展開", + "ui_depth_2": "展開 2 級", + "ui_depth_3": "展開 3 級", + "ui_fullscreen": "全屏", + "ui_theme": "主題", + "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ 無法加載思維導圖:缺少有效內容。", + "html_error_load_failed": "⚠️ 資源加載失敗,請稍后重試。", + "js_done": "完成", + "js_failed": "失敗", + "js_generating": "生成中...", + "js_filename": "思維導圖.png", + "js_upload_failed": "上傳失敗:", + "md_image_alt": "🧠 思維導圖", + "notification_waiting": "分析時間可能比預期稍長,請稍等,我們正在為您拼命繪圖中...", + }, + "ko-KR": { + "status_starting": "스마트 마인드맵이 시작되었습니다, 마인드맵을 생성 중입니다...", + "error_no_content": "유효한 사용자 메시지 내용을 가져올 수 없습니다.", + "error_text_too_short": "텍스트 내용이 너무 짧아({len}자), 효과적인 분석을 수행할 수 없습니다. 최소 {min_len}자 이상의 텍스트를 제공해 주세요.", + "status_analyzing": "스마트 마인드맵: 텍스트 구조 심층 분석 중...", + "status_drawing": "스마트 마인드맵: 그리기 완료!", + "notification_success": "마인드맵이 생성되었습니다, {user_name}님!", + "error_processing": "스마트 마인드맵 처리 실패: {error}", + "error_user_facing": "죄송합니다, 스마트 마인드맵 처리 중 오류가 발생했습니다: {error}.\n자세한 내용은 Open WebUI 백엔드 로그를 확인해 주세요.", + "status_failed": "스마트 마인드맵: 처리 실패.", + "notification_failed": "스마트 마인드맵 생성 실패, {user_name}님!", + "status_rendering_image": "스마트 마인드맵: 이미지 렌더링 중...", + "status_image_generated": "스마트 마인드맵: 이미지 생성됨!", + "notification_image_success": "마인드맵 이미지가 생성되었습니다, {user_name}님!", + "ui_title": "🧠 스마트 마인드맵", + "ui_user": "사용자:", + "ui_time": "시간:", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "-", + "ui_zoom_reset": "초기화", + "ui_zoom_in": "+", + "ui_depth_select": "레벨 확장", + "ui_depth_all": "모두 확장", + "ui_depth_2": "레벨 2", + "ui_depth_3": "레벨 3", + "ui_fullscreen": "전체 화면", + "ui_theme": "테마", + "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ 마인드맵을 로드할 수 없습니다: 유효한 내용이 없습니다.", + "html_error_load_failed": "⚠️ 리소스 로드 실패, 나중에 다시 시도해 주세요.", + "js_done": "완료", + "js_failed": "실패", + "js_generating": "생성 중...", + "js_filename": "mindmap.png", + "js_upload_failed": "업로드 실패: ", + "md_image_alt": "🧠 마인드맵", + "notification_waiting": "분석 시간이 예상보다 오래 걸리고 있습니다. 잠시만 기다려 주세요, 마인드맵을 생성 중입니다...", + }, + "ja-JP": { + "status_starting": "スマートマインドマップが起動しました。マインドマップを生成しています...", + "error_no_content": "有効なユーザーメッセージの内容を取得できませんでした。", + "error_text_too_short": "텍스트 내용이 너무 짧아({len}자), 효과적인 분석을 수행할 수 없습니다. 최소 {min_len}자 이상의 텍스트를 제공해 주세요.", + "status_analyzing": "スマートマインドマップ:テキスト構造を詳細に分析中...", + "status_drawing": "スマートマインドマップ:描画完了!", + "notification_success": "マインドマップが生成されました、{user_name}さん!", + "error_processing": "スマートマインドマップ処理失敗:{error}", + "error_user_facing": "申し訳ありません、スマートマインドマップの処理中にエラーが発生しました:{error}。\n詳細については、Open WebUIバックエンドログを確認してください。", + "status_failed": "スマートマインドマップ:処理失敗。", + "notification_failed": "スマートマインドマップ生成失敗、{user_name}さん!", + "status_rendering_image": "スマートマインドマップ:画像レンダ링中...", + "status_image_generated": "スマートマインドマップ:画像生成完了!", + "notification_image_success": "マインドマップ画像が生成されました、{user_name}さん!", + "ui_title": "🧠 スマートマインドマップ", + "ui_user": "ユーザー:", + "ui_time": "時間:", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "-", + "ui_zoom_reset": "リセット", + "ui_zoom_in": "+", + "ui_depth_select": "レベル展開", + "ui_depth_all": "すべて展開", + "ui_depth_2": "レベル2", + "ui_depth_3": "レベル3", + "ui_fullscreen": "全画面", + "ui_theme": "テーマ", + "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ マインドマップを読み込めません:有効なコンテンツがありません。", + "html_error_load_failed": "⚠️ リソースの読み込みに失敗しました。後でもう一度お試しください。", + "js_done": "完了", + "js_failed": "失敗", + "js_generating": "生成中...", + "js_filename": "mindmap.png", + "js_upload_failed": "アップロード失敗:", + "md_image_alt": "🧠 マインドマップ", + "notification_waiting": "分析に時間がかかっています。マインドマップを生成中ですので、もうしばらくお待ちください...", + }, + "fr-FR": { + "status_starting": "Smart Mind Map démarre, génération de la carte heuristique en cours...", + "error_no_content": "Impossible de récupérer le contenu valide du message utilisateur.", + "error_text_too_short": "Le contenu du texte est trop court ({len} caractères), impossible d'effectuer une analyse efficace. Veuillez fournir au moins {min_len} caractères de texte.", + "status_analyzing": "Smart Mind Map : Analyse approfondie de la structure du texte...", + "status_drawing": "Smart Mind Map : Dessin terminé !", + "notification_success": "La carte heuristique a été générée, {user_name} !", + "error_processing": "Échec du traitement de Smart Mind Map : {error}", + "error_user_facing": "Désolé, Smart Mind Map a rencontré une erreur lors du traitement : {error}.\nVeuillez vérifier les journaux backend d'Open WebUI pour plus de détails.", + "status_failed": "Smart Mind Map : Échec du traitement.", + "notification_failed": "Échec de la génération de la carte heuristique, {user_name} !", + "status_rendering_image": "Smart Mind Map : Rendu de l'image...", + "status_image_generated": "Smart Mind Map : Image générée !", + "notification_image_success": "L'image de la carte heuristique a été générée, {user_name} !", + "ui_title": "🧠 Smart Mind Map", + "ui_user": "Utilisateur :", + "ui_time": "Heure :", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "-", + "ui_zoom_reset": "Rénitialiser", + "ui_zoom_in": "+", + "ui_depth_select": "Niveau d'expansion", + "ui_depth_all": "Tout développer", + "ui_depth_2": "Niveau 2", + "ui_depth_3": "Niveau 3", + "ui_fullscreen": "Plein écran", + "ui_theme": "Thème", + "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ Impossible de charger la carte heuristique : contenu valide manquant.", + "html_error_load_failed": "⚠️ Échec du chargement des ressources, veuillez réessayer plus tard.", + "js_done": "Terminé", + "js_failed": "Échec", + "js_generating": "Génération...", + "js_filename": "carte_heuristique.png", + "js_upload_failed": "Échec du téléchargement : ", + "md_image_alt": "🧠 Carte Heuristique", + }, + "de-DE": { + "status_starting": "Smart Mind Map startet, Mindmap wird für Sie erstellt...", + "error_no_content": "Gültiger Inhalt der Benutzernachricht konnte nicht abgerufen werden.", + "error_text_too_short": "Der Textinhalt ist zu kurz ({len} Zeichen), eine effektive Analyse ist nicht möglich. Bitte geben Sie mindestens {min_len} Zeichen Text an.", + "status_analyzing": "Smart Mind Map: Detaillierte Analyse der Textstruktur...", + "status_drawing": "Smart Mind Map: Zeichnen abgeschlossen!", + "notification_success": "Mindmap wurde erstellt, {user_name}!", + "error_processing": "Smart Mind Map Verarbeitung fehlgeschlagen: {error}", + "error_user_facing": "Entschuldigung, bei der Verarbeitung von Smart Mind Map ist ein Fehler aufgetreten: {error}.\nBitte überprüfen Sie die Open WebUI Backend-Protokolle für weitere Details.", + "status_failed": "Smart Mind Map: Verarbeitung fehlgeschlagen.", + "notification_failed": "Erstellung der Mindmap fehlgeschlagen, {user_name}!", + "status_rendering_image": "Smart Mind Map: Bild wird gerendert...", + "status_image_generated": "Smart Mind Map: Bild erstellt!", + "notification_image_success": "Mindmap-Bild wurde erstellt, {user_name}!", + "ui_title": "🧠 Smart Mind Map", + "ui_user": "Benutzer:", + "ui_time": "Zeit:", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "-", + "ui_zoom_reset": "Zurücksetzen", + "ui_zoom_in": "+", + "ui_depth_select": "Ebene erweitern", + "ui_depth_all": "Alles erweitern", + "ui_depth_2": "Ebene 2", + "ui_depth_3": "Ebene 3", + "ui_fullscreen": "Vollbild", + "ui_theme": "Thema", + "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ Mindmap kann nicht geladen werden: Gültiger Inhalt fehlt.", + "html_error_load_failed": "⚠️ Ressourcenladen fehlgeschlagen, bitte versuchen Sie es später erneut.", + "js_done": "Fertig", + "js_failed": "Fehlgeschlagen", + "js_generating": "Generiere...", + "js_filename": "mindmap.png", + "js_upload_failed": "Upload fehlgeschlagen: ", + "md_image_alt": "🧠 Mindmap", + }, + "es-ES": { + "status_starting": "Smart Mind Map se está iniciando, generando mapa mental para usted...", + "error_no_content": "No se puede recuperar el contenido válido del mensaje del usuario.", + "error_text_too_short": "El contenido del texto es demasiado corto ({len} caracteres), no se puede realizar un análisis efectivo. Proporcione al menos {min_len} caracteres de texto.", + "status_analyzing": "Smart Mind Map: Analizando la estructura del texto en profundidad...", + "status_drawing": "Smart Mind Map: ¡Dibujo completado!", + "notification_success": "¡El mapa mental ha sido generado, {user_name}!", + "error_processing": "Falló el procesamiento de Smart Mind Map: {error}", + "error_user_facing": "Lo sentimos, Smart Mind Map encontró un error durante el procesamiento: {error}.\nConsulte los registros del backend de Open WebUI para más detalles.", + "status_failed": "Smart Mind Map: Procesamiento fallido.", + "notification_failed": "¡La generación del mapa mental falló, {user_name}!", + "status_rendering_image": "Smart Mind Map: Renderizando imagen...", + "status_image_generated": "Smart Mind Map: ¡Imagen generada!", + "notification_image_success": "¡La imagen del mapa mental ha sido generada, {user_name}!", + "ui_title": "🧠 Smart Mind Map", + "ui_user": "Usuario:", + "ui_time": "Hora:", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "-", + "ui_zoom_reset": "Restablecer", + "ui_zoom_in": "+", + "ui_depth_select": "Expandir Nivel", + "ui_depth_all": "Expandir Todo", + "ui_depth_2": "Nivel 2", + "ui_depth_3": "Nivel 3", + "ui_fullscreen": "Pantalla completa", + "ui_theme": "Tema", + "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ No se puede cargar el mapa mental: Falta contenido válido.", + "html_error_load_failed": "⚠️ Falló la carga de recursos, inténtelo de nuevo más tarde.", + "js_done": "Hecho", + "js_failed": "Fallido", + "js_generating": "Generando...", + "js_filename": "mapa_mental.png", + "js_upload_failed": "Carga fallida: ", + "md_image_alt": "🧠 Mapa Mental", + }, + "it-IT": { + "status_starting": "Smart Mind Map si sta avviando, generazione mappa mentale in corso...", + "error_no_content": "Impossibile recuperare il contenuto valido del messaggio utente.", + "error_text_too_short": "Il testo è troppo breve ({len} caratteri), impossibile eseguire un'analisi efficace. Fornire almeno {min_len} caratteri di testo.", + "status_analyzing": "Smart Mind Map: Analisi approfondita della struttura del testo...", + "status_drawing": "Smart Mind Map: Disegno completato!", + "notification_success": "La mappa mentale è stata generata, {user_name}!", + "error_processing": "Elaborazione Smart Mind Map fallita: {error}", + "error_user_facing": "Spiacenti, Smart Mind Map ha riscontrato un errore durante l'elaborazione: {error}.\nControllare i log del backend di Open WebUI per ulteriori dettagli.", + "status_failed": "Smart Mind Map: Elaborazione fallita.", + "notification_failed": "Generazione mappa mentale fallita, {user_name}!", + "status_rendering_image": "Smart Mind Map: Rendering immagine...", + "status_image_generated": "Smart Mind Map: Immagine generata!", + "notification_image_success": "L'immagine della mappa mentale è stata generata, {user_name}!", + "ui_title": "🧠 Smart Mind Map", + "ui_user": "Utente:", + "ui_time": "Ora:", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "-", + "ui_zoom_reset": "Reimposta", + "ui_zoom_in": "+", + "ui_depth_select": "Espandi Livello", + "ui_depth_all": "Espandi Tutto", + "ui_depth_2": "Livello 2", + "ui_depth_3": "Livello 3", + "ui_fullscreen": "Schermo intero", + "ui_theme": "Tema", + "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ Impossibile caricare la mappa mentale: Contenuto valido mancante.", + "html_error_load_failed": "⚠️ Caricamento risorse fallito, riprovare più tardi.", + "js_done": "Fatto", + "js_failed": "Fallito", + "js_generating": "Generazione...", + "js_filename": "mappa_mentale.png", + "js_upload_failed": "Caricamento fallito: ", + "md_image_alt": "🧠 Mappa Mentale", + }, + "vi-VN": { + "status_starting": "Smart Mind Map đang khởi động, đang tạo sơ đồ tư duy cho bạn...", + "error_no_content": "Không thể lấy nội dung tin nhắn người dùng hợp lệ.", + "error_text_too_short": "Nội dung văn bản quá ngắn ({len} ký tự), không thể thực hiện phân tích hiệu quả. Vui lòng cung cấp ít nhất {min_len} ký tự văn bản.", + "status_analyzing": "Smart Mind Map: Phân tích sâu cấu trúc văn bản...", + "status_drawing": "Smart Mind Map: Vẽ hoàn tất!", + "notification_success": "Sơ đồ tư duy đã được tạo, {user_name}!", + "error_processing": "Xử lý Smart Mind Map thất bại: {error}", + "error_user_facing": "Xin lỗi, Smart Mind Map đã gặp lỗi trong quá trình xử lý: {error}.\nVui lòng kiểm tra nhật ký backend Open WebUI để biết thêm chi tiết.", + "status_failed": "Smart Mind Map: Xử lý thất bại.", + "notification_failed": "Tạo sơ đồ tư duy thất bại, {user_name}!", + "status_rendering_image": "Smart Mind Map: Đang render hình ảnh...", + "status_image_generated": "Smart Mind Map: Hình ảnh đã tạo!", + "notification_image_success": "Hình ảnh sơ đồ tư duy đã được tạo, {user_name}!", + "ui_title": "🧠 Smart Mind Map", + "ui_user": "Người dùng:", + "ui_time": "Thời gian:", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "-", + "ui_zoom_reset": "Đặt lại", + "ui_zoom_in": "+", + "ui_depth_select": "Mở rộng Cấp độ", + "ui_depth_all": "Mở rộng Tất cả", + "ui_depth_2": "Cấp độ 2", + "ui_depth_3": "Cấp độ 3", + "ui_fullscreen": "Toàn màn hình", + "ui_theme": "Chủ đề", + "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ Không thể tải sơ đồ tư duy: Thiếu nội dung hợp lệ.", + "html_error_load_failed": "⚠️ Tải tài nguyên thất bại, vui lòng thử lại sau.", + "js_done": "Xong", + "js_failed": "Thất bại", + "js_generating": "Đang tạo...", + "js_filename": "sodo_tuduy.png", + "js_upload_failed": "Tải lên thất bại: ", + "md_image_alt": "🧠 Sơ đồ Tư duy", + }, + "id-ID": { + "status_starting": "Smart Mind Map sedang dimulai, membuat peta pikiran untuk Anda...", + "error_no_content": "Tidak dapat mengambil konten pesan pengguna yang valid.", + "error_text_too_short": "Konten teks terlalu pendek ({len} karakter), tidak dapat melakukan analisis efektif. Harap berikan setidaknya {min_len} karakter teks.", + "status_analyzing": "Smart Mind Map: Menganalisis struktur teks secara mendalam...", + "status_drawing": "Smart Mind Map: Menggambar selesai!", + "notification_success": "Peta pikiran telah dibuat, {user_name}!", + "error_processing": "Pemrosesan Smart Mind Map gagal: {error}", + "error_user_facing": "Maaf, Smart Mind Map mengalami kesalahan saat memproses: {error}.\nSilakan periksa log backend Open WebUI untuk detail lebih lanjut.", + "status_failed": "Smart Mind Map: Pemrosesan gagal.", + "notification_failed": "Pembuatan peta pikiran gagal, {user_name}!", + "status_rendering_image": "Smart Mind Map: Merender gambar...", + "status_image_generated": "Smart Mind Map: Gambar dibuat!", + "notification_image_success": "Gambar peta pikiran telah dibuat, {user_name}!", + "ui_title": "🧠 Smart Mind Map", + "ui_user": "Pengguna:", + "ui_time": "Waktu:", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "-", + "ui_zoom_reset": "Atur Ulang", + "ui_zoom_in": "+", + "ui_depth_select": "Perluas Level", + "ui_depth_all": "Perluas Semua", + "ui_depth_2": "Level 2", + "ui_depth_3": "Level 3", + "ui_fullscreen": "Layar Penuh", + "ui_theme": "Tema", + "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ Tidak dapat memuat peta pikiran: Konten valid hilang.", + "html_error_load_failed": "⚠️ Gagal memuat sumber daya, silakan coba lagi nanti.", + "js_done": "Selesai", + "js_failed": "Gagal", + "js_generating": "Membuat...", + "js_filename": "peta_pikiran.png", + "js_upload_failed": "Unggah gagal: ", + "md_image_alt": "🧠 Peta Pikiran", + }, + "ru-RU": { + "status_starting": "Умная интеллект-карта запускается, создаем карту для вас...", + "error_no_content": "Не удалось получить допустимое содержимое сообщения пользователя.", + "error_text_too_short": "Текст слишком короткий ({len} символов), невозможно провести эффективный анализ. Пожалуйста, предоставьте текст длиной не менее {min_len} символов.", + "status_analyzing": "Умная интеллект-карта: Глубокий анализ структуры текста...", + "status_drawing": "Умная интеллект-карта: Отрисовка завершена!", + "notification_success": "Интеллект-карта создана, {user_name}!", + "error_processing": "Ошибка обработки умной интеллект-карты: {error}", + "error_user_facing": "Извините, при обработке умной интеллект-карты произошла ошибка: {error}.\nПожалуйста, проверьте логи бэкенда Open WebUI для получения подробной информации.", + "status_failed": "Умная интеллект-карта: Обработка не удалась.", + "notification_failed": "Не удалось создать интеллект-карту, {user_name}!", + "status_rendering_image": "Умная интеллект-карта: Рендеринг изображения...", + "status_image_generated": "Умная интеллект-карта: Изображение создано!", + "notification_image_success": "Изображение интеллект-карты создано, {user_name}!", + "ui_title": "🧠 Умная интеллект-карта", + "ui_user": "Пользователь:", + "ui_time": "Время:", + "ui_download_png": "PNG", + "ui_download_svg": "SVG", + "ui_download_md": "Markdown", + "ui_zoom_out": "-", + "ui_zoom_reset": "Сброс", + "ui_zoom_in": "+", + "ui_depth_select": "Развернуть уровни", + "ui_depth_all": "Развернуть все", + "ui_depth_2": "Уровень 2", + "ui_depth_3": "Уровень 3", + "ui_fullscreen": "Полный экран", + "ui_theme": "Тема", + "ui_footer": "<b>Работает на</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", + "html_error_missing_content": "⚠️ Не удалось загрузить карту: Отсутствует допустимое содержимое.", + "html_error_load_failed": "⚠️ Ошибка загрузки ресурсов, пожалуйста, попробуйте позже.", + "js_done": "Готово", + "js_failed": "Ошибка", + "js_generating": "Генерация...", + "js_filename": "mindmap.png", + "js_upload_failed": "Ошибка загрузки: ", + "md_image_alt": "🧠 Интеллект-карта", + }, +} + +SYSTEM_PROMPT_MINDMAP_ASSISTANT = """ +You are a professional mind map generation assistant, capable of efficiently analyzing long-form text provided by users and structuring its core themes, key concepts, branches, and sub-branches into standard Markdown list syntax for rendering by Markmap.js. + +Please strictly follow these guidelines: +- **Language**: All output must be in the exact same language as the input text (the text you are analyzing). +- **Format Consistency**: Even if this system prompt is in English, if the user input is in Chinese, the mind map content must be in Chinese. If input is Russian, output Russian. +- **Format**: Your output must strictly be in Markdown list format, wrapped with ```markdown and ```. + - Use `#` to define the central theme (root node). + - Use `-` with two-space indentation to represent branches and sub-branches. +- **Root Node (Central Theme) — Strict Length Limits**: + - The `#` root node must be an ultra-compact title, like a newspaper headline. It should be a keyword or short phrase, NEVER a full sentence. + - **CJK scripts (Chinese, Japanese, Korean)**: Maximum **10 characters** (e.g., `# 老人缓解呼吸困难方法` ✓ / `# 老人在家时感到呼吸困难的缓解方法` ✗) + - **Latin-script languages (English, Spanish, French, Italian, Portuguese, Russian)**: Maximum **5 words or 35 characters** (e.g., `# Methods to Relieve Dyspnea` ✓ / `# How Elderly People Can Relieve Breathing Difficulty at Home` ✗) + - **German, Dutch or languages with long compound words**: Maximum **4 words or 30 characters** + - **Arabic, Hebrew and other RTL scripts**: Maximum **5 words or 25 characters** + - **All other languages**: Maximum **5 words or 30 characters** + - If the identified theme would exceed the limit, distill it further into the single most essential keyword or 2-3 word phrase. +- **Branch Node Content**: + - Identify main concepts as first-level list items. + - Identify supporting details or sub-concepts as nested list items. + - Node content should be concise and clear, avoiding verbosity. +- **Output Markdown syntax only**: Do not include any additional greetings, explanations, or guiding text. +- **If text is too short or cannot generate a valid mind map**: Output a simple Markdown list indicating inability to generate, for example: + ```markdown + # Unable to Generate Mind Map + - Reason: Insufficient or unclear text content + ``` +- **Awareness of Target Audience Layout**: You will be provided `Target Rendering Mode`. + - If `Target Rendering Mode` is `direct`: The client has massive horizontal space but limited scrolling vertically. Extract more first-level concepts to make the mind map spread wide like a sprawling fan, rather than deep single columns. + - If `Target Rendering Mode` is `legacy`: The client uses a narrow, portrait sidebar. Extract fewer top-level nodes, and break points into deeper, tighter sub-branches so the map grows vertically downwards. +""" + +USER_PROMPT_GENERATE_MINDMAP = """ +Please analyze the following long-form text and structure its core themes, key concepts, branches, and sub-branches into standard Markdown list syntax for Markmap.js rendering. + +--- +**User Context Information:** +User Name: {user_name} +Current Date & Time: {current_date_time_str} +Current Weekday: {current_weekday} +Current Timezone: {current_timezone_str} +User Language: {user_language} +Target Rendering Mode: Auto-adapting (Dynamic width based on viewport) +--- + +**Long-form Text Content:** +{long_text_content} +""" + + +def _resolve_language(valves, lang: str) -> str: + """Resolve the best matching language code from the TRANSLATIONS dict.""" + target_lang = lang + + # 0. Basic base language match (e.g. 'en', 'zh', 'ja') + if len(lang) == 2: + for supported_lang in TRANSLATIONS: + if supported_lang.startswith(lang): + return supported_lang + + # 1. Direct match + if target_lang in TRANSLATIONS: + return target_lang + + # 2. Variant fallback (explicit mapping) + # Mapping regional variants to their most complete translation set + fallback_map = { + "zh": "zh-CN", + "en": "en-US", + "ja": "ja-JP", + "ko": "ko-KR", + "zh-CN": "zh-CN", + "zh-HK": "zh-HK", + "zh-TW": "zh-TW", + "es-AR": "es-ES", + "es-MX": "es-ES", + "fr-CA": "fr-FR", + "en-CA": "en-US", + "en-GB": "en-US", + "en-AU": "en-US", + "de-AT": "de-DE", + } + if target_lang in fallback_map: + target_lang = fallback_map[target_lang] + if target_lang in TRANSLATIONS: + return target_lang + + # 3. Base language fallback (e.g. fr-BE -> fr-FR) + if "-" in lang: + base_lang = lang.split("-")[0] + # Check if base lang matches any supported translation + for supported_lang in TRANSLATIONS: + if supported_lang.startswith(base_lang): + return supported_lang + + return "en-US" + + +def _extract_text_content(content: Any) -> str: + """Normalize message content to a plain text string. + + Handles both simple string content and OpenAI-style multi-part + content arrays (e.g. [{"type": "text", "text": "hello"}]). + """ + if isinstance(content, str): + return content + if isinstance(content, list): + return "".join( + part.get("text", "") + for part in content + if isinstance(part, dict) and part.get("type") == "text" + ).strip() + return str(content) + + +def _get_translation(valves, lang: str, key: str, **kwargs) -> str: + """Retrieve a localized string by key, falling back to en-US on miss. + + Args: + valves: Plugin Valves instance (used by _resolve_language). + lang: BCP-47 language tag resolved from user context. + key: Translation key defined in the TRANSLATIONS dict. + **kwargs: Optional format arguments interpolated into the string. + + Returns: + Fully formatted localized string. + """ + target = _resolve_language(valves, lang) + trans_set = TRANSLATIONS.get(target, TRANSLATIONS["en-US"]) + text = trans_set.get(key, TRANSLATIONS["en-US"].get(key, key)) + return text.format(**kwargs) if kwargs else text + + +def _extract_markdown_syntax(content: str) -> str: + """Strip wrapping fenced code block markers from LLM output. + + Extracts the inner Markdown text from a ```markdown ... ``` block. + If no code fence is found, the raw content is returned as-is. + Also escapes '</script>' tags to prevent XSS when embedding in HTML. + """ + match = re.search( + r"```(?:markdown|md)?\s*(.*?)\s*```", content, re.DOTALL | re.IGNORECASE + ) + extracted = match.group(1).strip() if match else content.strip() + return extracted.replace("</script>", "<\\/script>") + + +HTML_WRAPPER_TEMPLATE = """ +<!-- OPENWEBUI_PLUGIN_OUTPUT --> +<!DOCTYPE html> +<html lang="{lang}"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <style> + body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + margin: 0; + padding: 12px; + background-color: transparent; + width: 100%; + box-sizing: border-box; + } + #main-container { + display: flex; + flex-direction: column; + align-items: stretch; + width: 100%; + } + .plugin-item { + width: 100%; + border-radius: 12px; + overflow: visible; + transition: all 0.3s ease; + } + .plugin-item:hover { + transform: translateY(-2px); + } + /* STYLES_INSERTION_POINT */ + </style> +</head> +<body> + <div id="main-container"> + <!-- CONTENT_INSERTION_POINT --> + </div> + <!-- SCRIPTS_INSERTION_POINT --> +</body> +</html> +""" + + +async def _emit_status(emitter, description: str, done: bool = False): + """Emit a status event to the OpenWebUI frontend. + + Args: + emitter: The __event_emitter__ callable injected by OpenWebUI. + description: Human-readable status message to display. + done: True marks the status as terminal (spinner stops). + """ + if emitter: + await emitter( + {"type": "status", "data": {"description": description, "done": done}} + ) + + +async def _emit_notification(emitter, content: str, ntype: str = "info"): + """Emit a toast notification event to the OpenWebUI frontend. + + Args: + emitter: The __event_emitter__ callable injected by OpenWebUI. + content: Notification body text. + ntype: Severity level — one of 'info', 'warning', 'error', 'success'. + """ + if emitter: + await emitter( + {"type": "notification", "data": {"type": ntype, "content": content}} + ) + + +async def _get_user_context( + __user__: dict, + __request__: Request, + valves: Any = None, + __event_call__: Callable = None, +) -> dict: + """Extract basic user context with safe fallbacks, matching Action logic perfectly.""" + # 1. Base identity + if isinstance(__user__, (list, tuple)): + user_data = __user__[0] if __user__ else {} + elif isinstance(__user__, dict): + user_data = __user__ + else: + user_data = {} + + user_id = user_data.get("id", "unknown") + user_name = user_data.get("name", "User") + + # Priority 4 (Lowest): User Profile language setting + user_language = user_data.get("language", "en-US") + + # Priority 3: Accept-Language from __request__ headers + if ( + __request__ + and hasattr(__request__, "headers") + and "accept-language" in __request__.headers + ): + raw_lang = __request__.headers.get("accept-language", "") + if raw_lang: + user_language = raw_lang.split(",")[0].split(";")[0] + + # Priority 2: Browser/Frontend Detection (via JS) + if __event_call__: + try: + js_code = """ + try { + return ( + document.documentElement.lang || + localStorage.getItem('locale') || + localStorage.getItem('language') || + navigator.language || + 'en-US' + ); + } catch (e) { + return 'en-US'; + } + """ + frontend_lang = await asyncio.wait_for( + __event_call__({"type": "execute", "data": {"code": js_code}}), + timeout=2.0, + ) + if frontend_lang and isinstance(frontend_lang, str): + logger.info(f"Frontend language detected via JS: {frontend_lang}") + user_language = frontend_lang + except Exception as e: + logger.warning( + f"Failed to retrieve frontend language via __event_call__: {e}" + ) + + return {"user_id": user_id, "user_name": user_name, "user_language": user_language} + + +SCRIPT_TEMPLATE_MINDMAP = """ +<script> + (function() { + const uniqueId = {unique_id_json}; + const i18n = {i18n_json}; + + const loadScriptOnce = (src, checkFn) => { + if (checkFn()) return Promise.resolve(); + return new Promise((resolve, reject) => { + const existing = document.querySelector(`script[data-src="${src}"]`); + if (existing) { + existing.addEventListener('load', () => resolve()); + existing.addEventListener('error', () => reject(new Error('Loading failed: ' + src))); + return; + } + const script = document.createElement('script'); + script.src = src; + script.async = true; + script.dataset.src = src; + script.onload = () => resolve(); + script.onerror = () => reject(new Error('Loading failed: ' + src)); + document.head.appendChild(script); + }); + }; + + const ensureMarkmapReady = () => + loadScriptOnce('https://cdn.jsdelivr.net/npm/d3@7', () => window.d3) + .then(() => loadScriptOnce('https://cdn.jsdelivr.net/npm/markmap-lib@0.17', () => window.markmap && window.markmap.Transformer)) + .then(() => loadScriptOnce('https://cdn.jsdelivr.net/npm/markmap-view@0.17', () => window.markmap && window.markmap.Markmap)); + + 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 = (wrapperEl, explicitTheme) => { + const parentClassTheme = getThemeFromParentClass(); + const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + const chosen = explicitTheme || parentClassTheme || (prefersDark ? 'dark' : 'light'); + wrapperEl.classList.toggle('theme-dark', chosen === 'dark'); + return chosen; + }; + + const renderMindmap = () => { + const containerEl = document.getElementById('markmap-container-' + uniqueId); + if (!containerEl || containerEl.dataset.markmapRendered) return; + + const sourceEl = document.getElementById('markdown-source-' + uniqueId); + if (!sourceEl) return; + + const markdownContent = sourceEl.textContent.trim(); + if (!markdownContent) { + containerEl.innerHTML = '<div class="error-message">' + i18n.html_error_missing_content + '</div>'; + return; + } + + ensureMarkmapReady().then(() => { + const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svgEl.style.width = '100%'; + svgEl.style.height = '100%'; + containerEl.innerHTML = ''; + containerEl.appendChild(svgEl); + + const { Transformer, Markmap } = window.markmap; + const transformer = new Transformer(); + const { root } = transformer.transform(markdownContent); + + const containerWidth = containerEl.clientWidth || window.innerWidth; + const containerHeight = containerEl.clientHeight || window.innerHeight; + const isPortrait = containerHeight >= containerWidth * 0.8; + + const style = (id) => ` + ${id} text, ${id} foreignObject { font-size: 16px; } + ${id} foreignObject { line-height: 1.6; } + ${id} foreignObject div { padding: 2px 0; } + ${id} foreignObject h1 { font-size: 24px; font-weight: 700; margin: 0 0 6px 0; border-bottom: 2px solid currentColor; padding-bottom: 4px; display: inline-block; } + ${id} foreignObject h2 { font-size: 18px; font-weight: 600; margin: 0 0 4px 0; } + ${id} foreignObject strong { font-weight: 700; } + ${id} foreignObject p { margin: 2px 0; } + `; + + let responsiveMaxWidth = Math.max(220, Math.floor(containerWidth * 0.35)); + let dynamicSpacingVertical = 12; + let dynamicSpacingHorizontal = 60; + + if (isPortrait) { + responsiveMaxWidth = Math.max(140, Math.floor(containerWidth * 0.35)); + dynamicSpacingVertical = 20; + } + + const options = { + autoFit: true, + style: style, + initialExpandLevel: 3, + zoom: true, + pan: true, + fitRatio: 0.95, + maxWidth: responsiveMaxWidth, + spacingVertical: dynamicSpacingVertical, + spacingHorizontal: dynamicSpacingHorizontal, + colorFreezeLevel: 2 + }; + + const markmapInstance = Markmap.create(svgEl, options, root); + + setTimeout(() => markmapInstance.fit(), 300); + + const resizeObserver = new ResizeObserver(entries => { + for (let entry of entries) { + if (entry.contentRect.width > 0 && entry.contentRect.height > 0) { + requestAnimationFrame(() => markmapInstance.fit()); + } + } + }); + resizeObserver.observe(containerEl); + + window['markmapInstance_' + uniqueId] = markmapInstance; + containerEl.dataset.markmapRendered = 'true'; + + setupControls({ + containerEl, + svgEl, + markmapInstance, + root + }); + + }).catch((error) => { + console.error('Markmap loading error:', error); + containerEl.innerHTML = '<div class="error-message">' + i18n.html_error_load_failed + '</div>'; + }); + }; + + const adjustLayout = () => { + const wrapper = document.querySelector('.mindmap-container-wrapper'); + const header = document.querySelector('.header'); + const contentArea = document.querySelector('.content-area'); + if (!wrapper || !header || !contentArea) return; + const headerH = header.getBoundingClientRect().height; + const totalH = wrapper.getBoundingClientRect().height; + const contentH = Math.max(totalH - headerH, 200); + contentArea.style.height = contentH + 'px'; + }; + + const setupControls = ({ containerEl, svgEl, markmapInstance, root }) => { + const downloadSvgBtn = document.getElementById('download-svg-btn-' + uniqueId); + const downloadPngBtn = document.getElementById('download-png-btn-' + uniqueId); + const downloadMdBtn = document.getElementById('download-md-btn-' + uniqueId); + const zoomInBtn = document.getElementById('zoom-in-btn-' + uniqueId); + const zoomOutBtn = document.getElementById('zoom-out-btn-' + uniqueId); + const zoomResetBtn = document.getElementById('zoom-reset-btn-' + uniqueId); + const depthSelect = document.getElementById('depth-select-' + uniqueId); + const fullscreenBtn = document.getElementById('fullscreen-btn-' + uniqueId); + const themeToggleBtn = document.getElementById('theme-toggle-btn-' + uniqueId); + + const wrapper = containerEl.closest('.mindmap-container-wrapper'); + let currentTheme = setTheme(wrapper); + + const showFeedback = (button, textOk = i18n.js_done, textFail = i18n.js_failed) => { + if (!button) return; + const buttonText = button.querySelector('.btn-text') || button; + const originalText = buttonText.textContent; + button.disabled = true; + buttonText.textContent = textOk; + setTimeout(() => { + buttonText.textContent = originalText; + button.disabled = false; + }, 1800); + }; + + const copyToClipboard = (content, button) => { + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(content).then(() => showFeedback(button), () => showFeedback(button, i18n.js_failed, i18n.js_failed)); + } else { + const textArea = document.createElement('textarea'); + textArea.value = content; + textArea.style.position = 'fixed'; + textArea.style.opacity = '0'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + document.execCommand('copy'); + showFeedback(button); + } catch (err) { + showFeedback(button, i18n.js_failed, i18n.js_failed); + } + document.body.removeChild(textArea); + } + }; + + const handleDownloadSVG = () => { + const clonedSvg = svgEl.cloneNode(true); + const style = document.createElement('style'); + style.textContent = ` + text { font-family: sans-serif; fill: ${currentTheme === 'dark' ? '#ffffff' : '#000000'}; } + foreignObject, .markmap-foreign, .markmap-foreign div { color: ${currentTheme === 'dark' ? '#ffffff' : '#000000'}; font-family: sans-serif; font-size: 14px; } + h1 { font-size: 22px; font-weight: 700; margin: 0; } + h2 { font-size: 18px; font-weight: 600; margin: 0; } + strong { font-weight: 700; } + .markmap-link { stroke: ${currentTheme === 'dark' ? '#cbd5e1' : '#546e7a'}; } + .markmap-node circle, .markmap-node rect { stroke: ${currentTheme === 'dark' ? '#94a3b8' : '#94a3b8'}; } + `; + clonedSvg.prepend(style); + const svgData = new XMLSerializer().serializeToString(clonedSvg); + copyToClipboard(svgData, downloadSvgBtn); + }; + + const handleDownloadMD = () => { + const markdownContent = document.getElementById('markdown-source-' + uniqueId)?.textContent || ''; + copyToClipboard(markdownContent, downloadMdBtn); + }; + + const handleDownloadPNG = () => { + const btn = downloadPngBtn; + const originalText = btn.textContent; + btn.textContent = i18n.js_generating; + btn.disabled = true; + + try { + const clonedSvg = svgEl.cloneNode(true); + clonedSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); + const rect = svgEl.getBoundingClientRect(); + const width = rect.width || 800; + const height = rect.height || 600; + clonedSvg.setAttribute('width', width); + clonedSvg.setAttribute('height', height); + + const foreignObjects = clonedSvg.querySelectorAll('foreignObject'); + foreignObjects.forEach(fo => { + const text = fo.textContent || ''; + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + textEl.setAttribute('x', fo.getAttribute('x') || '0'); + textEl.setAttribute('y', (parseFloat(fo.getAttribute('y') || '0') + 14).toString()); + textEl.setAttribute('fill', currentTheme === 'dark' ? '#ffffff' : '#000000'); + textEl.setAttribute('font-family', 'sans-serif'); + textEl.setAttribute('font-size', '14'); + textEl.textContent = text.trim(); + g.appendChild(textEl); + fo.parentNode.replaceChild(g, fo); + }); + + const style = document.createElementNS('http://www.w3.org/2000/svg', 'style'); + style.textContent = ` + text { font-family: sans-serif; font-size: 14px; fill: ${currentTheme === 'dark' ? '#ffffff' : '#000000'}; } + .markmap-link { fill: none; stroke: ${currentTheme === 'dark' ? '#cbd5e1' : '#546e7a'}; stroke-width: 2; } + .markmap-node circle { stroke: ${currentTheme === 'dark' ? '#94a3b8' : '#94a3b8'}; stroke-width: 2; } + `; + clonedSvg.insertBefore(style, clonedSvg.firstChild); + + const bgRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + bgRect.setAttribute('width', '100%'); + bgRect.setAttribute('height', '100%'); + bgRect.setAttribute('fill', currentTheme === 'dark' ? '#1f2937' : '#ffffff'); + clonedSvg.insertBefore(bgRect, clonedSvg.firstChild); + + const svgData = new XMLSerializer().serializeToString(clonedSvg); + const svgBase64 = btoa(unescape(encodeURIComponent(svgData))); + const img = new Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + const scale = 3; + canvas.width = width * scale; + canvas.height = height * scale; + const ctx = canvas.getContext('2d'); + ctx.scale(scale, scale); + ctx.drawImage(img, 0, 0, width, height); + + canvas.toBlob((blob) => { + if (!blob) return; + const a = document.createElement('a'); + a.download = i18n.js_filename; + a.href = URL.createObjectURL(blob); + a.click(); + URL.revokeObjectURL(a.href); + btn.textContent = originalText; + btn.disabled = false; + showFeedback(btn); + }, 'image/png'); + }; + img.src = 'data:image/svg+xml;base64,' + svgBase64; + } catch (err) { + btn.textContent = originalText; + btn.disabled = false; + showFeedback(btn, i18n.js_failed, i18n.js_failed); + } + }; + + const handleZoom = (dir) => { + if (dir === 'reset') markmapInstance.fit(); + else if (markmapInstance.rescale) { + markmapInstance.rescale(dir === 'in' ? 1.25 : 0.8); + } + }; + + const handleDepthChange = (e) => { + const level = parseInt(e.target.value, 10); + const expandLevel = level === 0 ? Infinity : level; + + const applyFold = (node, currentDepth) => { + if (!node) return; + if (!node.payload) node.payload = {}; + node.payload.fold = currentDepth >= expandLevel ? 1 : 0; + if (node.children) node.children.forEach(c => applyFold(c, currentDepth + 1)); + }; + + const cleanRoot = JSON.parse(JSON.stringify(root)); + applyFold(cleanRoot, 0); + markmapInstance.setOptions({ initialExpandLevel: expandLevel }); + markmapInstance.setData(cleanRoot); + setTimeout(() => markmapInstance.fit(), 50); + }; + + const handleFullscreen = () => { + const el = wrapper || containerEl; + if (!document.fullscreenElement) { + el.requestFullscreen().catch(() => containerEl.requestFullscreen()); + } else { + document.exitFullscreen(); + } + }; + + downloadSvgBtn?.addEventListener('click', () => handleDownloadSVG()); + downloadMdBtn?.addEventListener('click', () => handleDownloadMD()); + downloadPngBtn?.addEventListener('click', () => handleDownloadPNG()); + zoomInBtn?.addEventListener('click', () => handleZoom('in')); + zoomOutBtn?.addEventListener('click', () => handleZoom('out')); + zoomResetBtn?.addEventListener('click', () => handleZoom('reset')); + depthSelect?.addEventListener('change', handleDepthChange); + fullscreenBtn?.addEventListener('click', handleFullscreen); + themeToggleBtn?.addEventListener('click', () => { + currentTheme = currentTheme === 'dark' ? 'light' : 'dark'; + setTheme(wrapper, currentTheme); + }); + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', renderMindmap); + } else { + renderMindmap(); + } + })(); + </script> +""" + + class Tools: + """Smart Mind Map Tool — OpenWebUI Tool plugin. + + Proactively transforms multi-turn conversation content into an + interactive, browser-rendered mind map powered by Markmap. + The AI decides when a mind map would be beneficial and invokes + this tool automatically, unlike the Action variant which requires + manual user initiation. + + Key features: + - Full UI parity with the Smart Mind Map Action. + - Multi-layer language detection (JS frontend > HTTP header > profile). + - 5-second waiting notification to reassure users during slow LLM calls. + - Configurable via Valves: model, min text length, message count, + and fallback language. + """ + class Valves(BaseModel): - MODEL_ID: str = Field(default="", description="The model ID to use for mind map generation. If empty, uses the current conversation model.") - MIN_TEXT_LENGTH: int = Field(default=50, description="Minimum text length required for analysis.") - SHOW_STATUS: bool = Field(default=True, description="Whether to show status messages.") + MODEL_ID: str = Field( + default="", + description="Specific model ID to use for mind map analysis (e.g., 'gpt-4o'). If empty, uses the current conversation model.", + ) + MIN_TEXT_LENGTH: int = Field( + default=100, + description="Minimum text length (character count) required for mind map analysis.", + ) + MESSAGE_COUNT: int = Field( + default=12, + description="Number of recent messages to use for generation (0 for all messages).", + ) def __init__(self): + """Initialise plugin state: Valves config and internal lookup maps.""" self.valves = self.Valves() - self.__translations = { - "en-US": { - "status_analyzing": "Smart Mind Map: Analyzing text structure...", - "status_drawing": "Smart Mind Map: Drawing completed!", - "notification_success": "Mind map has been generated, {user_name}!", - "error_text_too_short": "Text content is too short ({len} characters). Min: {min_len}.", - "error_user_facing": "Sorry, Smart Mind Map encountered an error: {error}", - "status_failed": "Smart Mind Map: Failed.", - "ui_title": "🧠 Smart Mind Map", - "ui_download_png": "PNG", - "ui_download_svg": "SVG", - "ui_download_md": "Markdown", - "ui_zoom_out": "Zoom Out", - "ui_zoom_reset": "Reset", - "ui_zoom_in": "Zoom In", - "ui_depth_select": "Expand Level", - "ui_depth_all": "All", - "ui_depth_2": "L2", - "ui_depth_3": "L3", - "ui_fullscreen": "Fullscreen", - "ui_theme": "Theme", - "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", - "html_error_missing_content": "⚠️ Missing content.", - "html_error_load_failed": "⚠️ Resource load failed.", - "js_done": "Done", - }, - "zh-CN": { - "status_analyzing": "思维导图:深入分析文本结构...", - "status_drawing": "思维导图:绘制完成!", - "notification_success": "思维导图已生成,{user_name}!", - "error_text_too_short": "文本内容过短({len}字符),请提供至少{min_len}字符。", - "error_user_facing": "抱歉,思维导图处理出错:{error}", - "status_failed": "思维导图:处理失败。", - "ui_title": "🧠 智能思维导图", - "ui_download_png": "PNG", - "ui_download_svg": "SVG", - "ui_download_md": "Markdown", - "ui_zoom_out": "缩小", - "ui_zoom_reset": "重置", - "ui_zoom_in": "放大", - "ui_depth_select": "展开层级", - "ui_depth_all": "全部", - "ui_depth_2": "2级", - "ui_depth_3": "3级", - "ui_fullscreen": "全屏", - "ui_theme": "主题", - "ui_footer": "<b>Powered by</b> <a href='https://markmap.js.org/' target='_blank' rel='noopener noreferrer'>Markmap</a>", - "html_error_missing_content": "⚠️ 缺少有效内容。", - "html_error_load_failed": "⚠️ 资源加载失败。", - "js_done": "完成", - } + self.weekday_map = { + "Monday": "Monday", + "Tuesday": "Tuesday", + "Wednesday": "Wednesday", + "Thursday": "Thursday", + "Friday": "Friday", + "Saturday": "Saturday", + "Sunday": "Sunday", + } + self.fallback_map = { + "es-AR": "es-ES", + "es-MX": "es-ES", + "fr-CA": "fr-FR", + "en-CA": "en-US", + "en-GB": "en-US", + "en-AU": "en-US", + "de-AT": "de-DE", } - self.__system_prompt = """You are a professional mind map assistant. Analyze text and output Markdown list syntax for Markmap.js. -Guidelines: -- Root node (#) must be ultra-compact (max 10 chars for CJK, 5 words for Latin). -- Use '-' with 2-space indentation. -- Output ONLY Markdown wrapped in ```markdown. -- Match the language of the input text.""" - self.__css_template = """ + CSS_TEMPLATE = """ :root { - --primary-color: #1e88e5; --secondary-color: #43a047; --background-color: #f4f6f8; - --card-bg-color: #ffffff; --text-color: #000000; --link-color: #546e7a; - --node-stroke-color: #90a4ae; --muted-text-color: #546e7a; --border-color: #e0e0e0; - --shadow: 0 4px 12px rgba(0, 0, 0, 0.05); --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --primary-color: #1e88e5; + --secondary-color: #43a047; + --background-color: #f4f6f8; + --card-bg-color: #ffffff; + --text-color: #000000; + --link-color: #546e7a; + --node-stroke-color: #90a4ae; + --muted-text-color: #546e7a; + --border-color: #e0e0e0; + --header-gradient: linear-gradient(135deg, var(--secondary-color), var(--primary-color)); + --shadow: 0 10px 20px rgba(0, 0, 0, 0.06); + --border-radius: 12px; + --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .theme-dark { - --primary-color: #3b82f6; --secondary-color: #22c55e; --background-color: #0d1117; - --card-bg-color: #161b22; --text-color: #ffffff; --link-color: #58a6ff; - --node-stroke-color: #8b949e; --muted-text-color: #7d8590; --border-color: #30363d; + --primary-color: #64b5f6; + --secondary-color: #81c784; + --background-color: #111827; + --card-bg-color: #1f2937; + --text-color: #ffffff; + --link-color: #cbd5e1; + --node-stroke-color: #94a3b8; + --muted-text-color: #9ca3af; + --border-color: #374151; + --header-gradient: linear-gradient(135deg, #0ea5e9, #22c55e); + --shadow: 0 10px 20px rgba(0, 0, 0, 0.3); } - html, body { margin: 0; padding: 0; width: 100%; height: 600px; background: transparent; overflow: hidden; font-family: var(--font-family); } - .mindmap-wrapper { display: flex; flex-direction: column; width: 100%; height: 100%; background: var(--card-bg-color); border: 1px solid var(--border-color); border-radius: 12px; overflow: hidden; box-shadow: var(--shadow); } - .header { display: flex; align-items: center; padding: 8px 16px; border-bottom: 1px solid var(--border-color); background: var(--card-bg-color); flex-shrink: 0; gap: 12px; } - .header h1 { margin: 0; font-size: 1rem; flex-grow: 1; color: var(--text-color); } - .btn-group { display: flex; gap: 2px; background: var(--background-color); padding: 2px; border-radius: 6px; } - .control-btn { border: none; background: transparent; color: var(--text-color); padding: 4px 8px; cursor: pointer; border-radius: 4px; font-size: 0.8rem; opacity: 0.7; } - .control-btn:hover { background: var(--card-bg-color); opacity: 1; } - .content { flex-grow: 1; position: relative; } - .markmap-container { position: absolute; top:0; left:0; right:0; bottom:0; } - svg text { fill: var(--text-color) !important; } - svg .markmap-link { stroke: var(--link-color) !important; } - """ + html, body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + background-color: transparent !important; + overflow: hidden; + } + .mindmap-container-wrapper { + font-family: var(--font-family); + line-height: 1.6; + color: var(--text-color); + margin: 0; + padding: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + display: flex; + flex-direction: column; + background: var(--card-bg-color); + width: 100%; + aspect-ratio: 16 / 9; + min-height: 300px; + max-height: 800px; + box-sizing: border-box; + overflow: hidden; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + box-shadow: var(--shadow); + } + .header { + background: var(--card-bg-color); + color: var(--text-color); + padding: 8px 16px; + display: flex; + flex-direction: column; + gap: 8px; + flex-shrink: 0; + border-bottom: 1px solid var(--border-color); + z-index: 10; + } + .header-top { + display: flex; + align-items: center; + gap: 12px; + } + .header h1 { + margin: 0; + font-size: 1.1em; + font-weight: 600; + letter-spacing: 0.3px; + display: flex; + align-items: center; + gap: 8px; + } + .header-credits { + font-size: 0.8em; + color: var(--muted-text-color); + opacity: 0.8; + white-space: nowrap; + } + .header-credits a { + color: var(--primary-color); + text-decoration: none; + border-bottom: 1px dotted var(--link-color); + } + + .content-area { + padding: 0; + flex: 1 1 0; + background: var(--card-bg-color); + position: relative; + overflow: hidden; + width: 100%; + min-height: 0; + } + .markmap-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--card-bg-color); + } + .markmap-container svg { + width: 100%; + height: 100%; + display: block; + } + .markmap-container svg text { + fill: var(--text-color) !important; + font-family: var(--font-family); + } + .markmap-container svg foreignObject, + .markmap-container svg .markmap-foreign, + .markmap-container svg .markmap-foreign div { + color: var(--text-color) !important; + font-family: var(--font-family); + } + .markmap-container svg .markmap-link { + stroke: var(--link-color) !important; + stroke-opacity: 0.6; + } + .theme-dark .markmap-node circle { + fill: var(--card-bg-color) !important; + } + .markmap-container svg .markmap-node circle, + .markmap-container svg .markmap-node rect { + stroke: var(--node-stroke-color) !important; + } + .control-rows { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; + margin-left: auto; + } + .btn-group { + display: inline-flex; + gap: 4px; + align-items: center; + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 2px; + background: var(--background-color); + } + .control-btn { + background-color: transparent; + color: var(--text-color); + border: none; + padding: 4px 10px; + border-radius: 4px; + font-size: 0.85em; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; + height: 28px; + box-sizing: border-box; + opacity: 0.8; + } + .control-btn:hover { + background-color: var(--card-bg-color); + opacity: 1; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + } + .control-btn.primary { + background-color: var(--primary-color); + color: white; + opacity: 1; + } + .control-btn.primary:hover { + box-shadow: 0 2px 5px rgba(30,136,229,0.3); + } + select.control-btn { + appearance: none; + padding-right: 28px; + background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23FFFFFF%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + background-size: 10px; + } + .control-btn option { + background-color: var(--card-bg-color); + color: var(--text-color); + } + @media screen and (max-width: 768px) { + .mindmap-container-wrapper { + aspect-ratio: 4 / 5; + min-height: 480px; + max-height: 85vh; + } + .header { flex-direction: column; gap: 10px; } + .btn-group { padding: 2px; } + .control-btn { padding: 4px 6px; font-size: 0.75em; height: 28px; } + select.control-btn { padding-right: 20px; background-position: right 4px center; } + } + """ - self.__content_template = """ - <div class="mindmap-wrapper"> + CONTENT_TEMPLATE = """ + <div class="mindmap-container-wrapper"> <div class="header"> - <h1>{t_ui_title}</h1> - <div class="btn-group"> - <button id="z-in-{uid}" class="control-btn">+</button> - <button id="z-out-{uid}" class="control-btn">-</button> - <button id="z-res-{uid}" class="control-btn">↺</button> + <div class="header-top"> + <h1>{t_ui_title}</h1> + <div class="header-credits"> + <span>{t_ui_footer}</span> + </div> + <div class="control-rows"> + <div class="btn-group"> + <button id="download-png-btn-{unique_id}" class="control-btn primary" title="{t_ui_download_png}">PNG</button> + <button id="download-svg-btn-{unique_id}" class="control-btn" title="{t_ui_download_svg}">SVG</button> + <button id="download-md-btn-{unique_id}" class="control-btn" title="{t_ui_download_md}">MD</button> + </div> + <div class="btn-group"> + <button id="zoom-out-btn-{unique_id}" class="control-btn" title="{t_ui_zoom_out}">-</button> + <button id="zoom-reset-btn-{unique_id}" class="control-btn" title="{t_ui_zoom_reset}">↺</button> + <button id="zoom-in-btn-{unique_id}" class="control-btn" title="{t_ui_zoom_in}">+</button> + </div> + <div class="btn-group"> + <select id="depth-select-{unique_id}" class="control-btn" title="{t_ui_depth_select}"> + <option value="0">{t_ui_depth_all}</option> + <option value="2">{t_ui_depth_2}</option> + <option value="3" selected>{t_ui_depth_3}</option> + </select> + <button id="fullscreen-btn-{unique_id}" class="control-btn" title="{t_ui_fullscreen}">⛶</button> + <button id="theme-toggle-btn-{unique_id}" class="control-btn" title="{t_ui_theme}">◑</button> + </div> + </div> </div> - <div class="btn-group"> - <select id="d-sel-{uid}" class="control-btn"> - <option value="0">{t_ui_depth_all}</option> - <option value="2">{t_ui_depth_2}</option> - <option value="3" selected>{t_ui_depth_3}</option> - </select> - </div> - <button id="t-tog-{uid}" class="control-btn">◐</button> </div> - <div class="content"><div class="markmap-container" id="mm-{uid}"></div></div> + <div class="content-area"> + <div class="markmap-container" id="markmap-container-{unique_id}"></div> + </div> </div> - <script type="text/template" id="src-{uid}">{md}</script> - """ + + <script type="text/template" id="markdown-source-{unique_id}">{markdown_syntax}</script> + """ async def generate_mind_map( self, - text: str, - __user__: Optional[Dict[str, Any]] = None, - __metadata__: Optional[Dict[str, Any]] = None, - __event_emitter__: Optional[Callable[[Any], Awaitable[None]]] = None, - __request__: Optional[Request] = None, + __user__: dict = {}, + __event_emitter__: Callable = None, + __event_call__: Callable = None, + __request__: Request = None, + __messages__: list = [], + __metadata__: dict = {}, ) -> Any: - user_ctx = await self.__get_user_context(__user__, __request__) - lang = user_ctx["lang"] - name = user_ctx["name"] + """Entry point invoked by the AI to generate an interactive mind map. - if len(text) < self.valves.MIN_TEXT_LENGTH: - return f"⚠️ {self.__get_t(lang, 'error_text_too_short', len=len(text), min_len=self.valves.MIN_TEXT_LENGTH)}" + Aggregates conversation messages from the __messages__ injection, + calls the configured LLM to produce a Markmap-compatible Markdown + outline, then renders it as a self-contained HTML response embedded + directly into the chat. - await self.__emit_status(__event_emitter__, self.__get_t(lang, "status_analyzing"), False) + OpenWebUI injects __messages__ directly with the full conversation + history. The MESSAGE_COUNT valve controls how many recent messages + are included (0 = all). + + A background timer fires a user-visible notification if LLM analysis + exceeds 5 seconds, ensuring the user is informed during slow responses. + + Args: + __user__: OpenWebUI user context dict. + __event_emitter__: Async callable for status/notification events. + __event_call__: Async callable for JS execution (language detection). + __request__: Starlette Request — used for Accept-Language header. + __messages__: Full conversation history injected by OpenWebUI. + __metadata__: OpenWebUI metadata bag containing model ID, etc. + + Returns: + HTMLResponse with the full mind map page on success, or an error string. + """ + user_ctx = await _get_user_context( + __user__, __request__, self.valves, __event_call__ + ) + user_lang = user_ctx["user_language"] + user_name = user_ctx["user_name"] + + # Aggregate conversation messages from __messages__ (OpenWebUI direct injection) + target_text = "" + all_msgs = __messages__ or [] + + if all_msgs: + count = self.valves.MESSAGE_COUNT + if count > 1: + recent = all_msgs[-count:] + else: + # 0: all messages + recent = all_msgs + + aggregated = [] + for msg in recent: + # Filter out messages that don't have user-visible content + # or are internal tool calls to avoid noise + role = msg.get("role") + content = _extract_text_content(msg.get("content", "")) + + if content and role in ["user", "assistant"]: + prefix = "User: " if role == "user" else "Assistant: " + aggregated.append(f"{prefix}{content}") + + if aggregated: + target_text = "\n\n".join(aggregated) + logger.info(f"Aggregated {len(aggregated)} messages for mind map.") + + await _emit_status( + __event_emitter__, + _get_translation(self.valves, user_lang, "status_starting"), + False, + ) + + if not target_text or len(target_text) < self.valves.MIN_TEXT_LENGTH: + msg = _get_translation( + self.valves, + user_lang, + "error_text_too_short", + len=len(target_text), + min_len=self.valves.MIN_TEXT_LENGTH, + ) + await _emit_notification(__event_emitter__, msg, "warning") + return f"⚠️ {msg}" + + await _emit_status( + __event_emitter__, + _get_translation(self.valves, user_lang, "status_analyzing"), + False, + ) + + async def _notify_waiting(): + try: + await asyncio.sleep(5.0) + await _emit_notification( + __event_emitter__, + _get_translation(self.valves, user_lang, "notification_waiting"), + "info", + ) + except asyncio.CancelledError: + pass + + waiting_task = asyncio.create_task(_notify_waiting()) try: - target_model = self.valves.MODEL_ID or (__metadata__.get("model_id") if __metadata__ else "") - llm_payload = { + target_model = self.valves.MODEL_ID + if not target_model: + meta_model = __metadata__.get("model", "") + if isinstance(meta_model, dict): + target_model = meta_model.get("id", "gpt-4o") + elif isinstance(meta_model, str) and meta_model.strip(): + target_model = meta_model + target_model = target_model or "gpt-4o" + # Prepare prompt context + tz_str = os.environ.get("TZ", "UTC") + now = datetime.now(ZoneInfo(tz_str)) + current_date_time_str = now.strftime("%Y-%m-%d %H:%M:%S") + current_weekday = now.strftime("%A") + + resolved_lang = _resolve_language(self.valves, user_lang) + + prompt = ( + USER_PROMPT_GENERATE_MINDMAP.replace("{user_name}", user_name) + .replace("{current_date_time_str}", current_date_time_str) + .replace("{current_weekday}", current_weekday) + .replace("{current_timezone_str}", tz_str) + .replace("{user_language}", resolved_lang) + .replace("{long_text_content}", target_text) + ) + + payload = { "model": target_model, "messages": [ - {"role": "system", "content": self.__system_prompt}, - {"role": "user", "content": f"Language: {lang}\nText: {text}"}, + {"role": "system", "content": SYSTEM_PROMPT_MINDMAP_ASSISTANT}, + {"role": "user", "content": prompt}, ], "temperature": 0.5, + "stream": False, + } + user_obj = Users.get_user_by_id(user_ctx["user_id"]) + response = await generate_chat_completion(__request__, payload, user_obj) + assistant_content = response["choices"][0]["message"]["content"] + markdown_syntax = _extract_markdown_syntax(assistant_content) + + unique_id = f"mm_{int(time.time())}" + ui_trans = { + f"t_{k}": _get_translation(self.valves, user_lang, k) + for k in TRANSLATIONS["en-US"] + if k.startswith("ui_") } - user_obj = Users.get_user_by_id(user_ctx["id"]) - response = await generate_chat_completion(__request__, llm_payload, user_obj) - md_content = self.__extract_md(response["choices"][0]["message"]["content"]) + html_body = self.CONTENT_TEMPLATE.replace("{unique_id}", unique_id).replace( + "{markdown_syntax}", markdown_syntax + ) + for k, v in ui_trans.items(): + html_body = html_body.replace(f"{{{k}}}", v) + js_trans = { + k: v + for k, v in TRANSLATIONS.get( + _resolve_language(self.valves, user_lang), TRANSLATIONS["en-US"] + ).items() + if k.startswith("js_") or k.startswith("html_") + } - uid = str(int(time.time() * 1000)) - ui_t = {f"t_{k}": self.__get_t(lang, k) for k in self.__translations["en-US"] if k.startswith("ui_")} - - html_body = self.__content_template.format(uid=uid, md=md_content, **ui_t) - - script = f""" - <script> - (function() {{ - const uid = "{uid}"; - const load = (s, c) => c() ? Promise.resolve() : new Promise((r,e) => {{ - const t = document.createElement('script'); t.src = s; t.onload = r; t.onerror = e; document.head.appendChild(t); - }}); - const init = () => load('https://cdn.jsdelivr.net/npm/d3@7', () => window.d3) - .then(() => load('https://cdn.jsdelivr.net/npm/markmap-lib@0.17', () => window.markmap?.Transformer)) - .then(() => load('https://cdn.jsdelivr.net/npm/markmap-view@0.17', () => window.markmap?.Markmap)) - .then(() => {{ - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.style.width = svg.style.height = '100%'; - const cnt = document.getElementById('mm-'+uid); cnt.appendChild(svg); - const {{ Transformer, Markmap }} = window.markmap; - const {{ root }} = new Transformer().transform(document.getElementById('src-'+uid).textContent); - const mm = Markmap.create(svg, {{ autoFit: true, initialExpandLevel: 3 }}, root); - document.getElementById('z-in-'+uid).onclick = () => mm.rescale(1.25); - document.getElementById('z-out-'+uid).onclick = () => mm.rescale(0.8); - document.getElementById('z-res-'+uid).onclick = () => mm.fit(); - document.getElementById('t-tog-'+uid).onclick = () => document.body.classList.toggle('theme-dark'); - document.getElementById('d-sel-'+uid).onchange = (e) => {{ - mm.setOptions({{ initialExpandLevel: parseInt(e.target.value) || 99 }}); mm.setData(root); mm.fit(); - }}; - window.addEventListener('resize', () => mm.fit()); - }}); - if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init(); - }})(); - </script> - """ + js_code = SCRIPT_TEMPLATE_MINDMAP.replace( + "{unique_id_json}", json.dumps(unique_id) + ).replace("{i18n_json}", json.dumps(js_trans, ensure_ascii=False)) - final_html = f"<!DOCTYPE html><html lang='{lang}'><head><style>{self.__css_template}</style></head><body>{html_body}{script}</body></html>" - - await self.__emit_status(__event_emitter__, self.__get_t(lang, "status_drawing"), True) - await self.__emit_notification(__event_emitter__, self.__get_t(lang, "notification_success", user_name=name), "success") + full_html = ( + HTML_WRAPPER_TEMPLATE.replace("{lang}", user_lang[0:2]) + .replace("/* STYLES_INSERTION_POINT */", self.CSS_TEMPLATE) + .replace("<!-- CONTENT_INSERTION_POINT -->", html_body) + .replace("<!-- SCRIPTS_INSERTION_POINT -->", js_code) + ) - return (final_html.strip(), {"Content-Disposition": "inline", "Content-Type": "text/html"}) + waiting_task.cancel() + await _emit_status( + __event_emitter__, + _get_translation(self.valves, user_lang, "status_drawing"), + True, + ) + return HTMLResponse( + content=full_html, headers={"Content-Disposition": "inline"} + ) except Exception as e: - logger.error(f"Mind Map Error: {e}", exc_info=True) - await self.__emit_status(__event_emitter__, self.__get_t(lang, "status_failed"), True) - return f"❌ {self.__get_t(lang, 'error_user_facing', error=str(e))}" - - async def __get_user_context(self, __user__, __request__) -> Dict[str, str]: - u = __user__ or {} - lang = u.get("language") or (__request__.headers.get("accept-language") or "en-US").split(",")[0].split(";")[0] - return {"id": u.get("id", "unknown"), "name": u.get("name", "User"), "lang": lang} - - def __get_t(self, lang: str, key: str, **kwargs) -> str: - base = lang.split("-")[0] - t = self.__translations.get(lang, self.__translations.get(base, self.__translations["en-US"])).get(key, key) - return t.format(**kwargs) if kwargs else t - - def __extract_md(self, content: str) -> str: - match = re.search(r"```markdown\s*(.*?)\s*```", content, re.DOTALL) - return (match.group(1).strip() if match else content.strip()).replace("</script>", "<\\/script>") - - async def __emit_status(self, emitter, description: str, done: bool): - if self.valves.SHOW_STATUS and emitter: - await emitter({"type": "status", "data": {"description": description, "done": done}}) - - async def __emit_notification(self, emitter, content: str, ntype: str): - if emitter: - await emitter({"type": "notification", "data": {"type": ntype, "content": content}}) + waiting_task.cancel() + logger.error(f"Generate Mind Map failed: {e}") + await _emit_status(__event_emitter__, f"Error: {e}", True) + return f"❌ {e}" diff --git a/plugins/tools/smart-mind-map-tool/v1.0.0.md b/plugins/tools/smart-mind-map-tool/v1.0.0.md new file mode 100644 index 0000000..1e22f16 --- /dev/null +++ b/plugins/tools/smart-mind-map-tool/v1.0.0.md @@ -0,0 +1,23 @@ +# Smart Mind Map Tool v1.0.0 Release Notes + +## Overview + +The Smart Mind Map Tool represents a strategic evolution of our popular Action plugin, fully optimized for the tool-calling era. Powered by OpenWebUI 0.8.0's Rich UI enhancements, it transforms knowledge visualization from a manual task into an autonomous AI capability, intelligently deciding when a visual breakdown will most benefit your workflow. + +## New Features + +- **AI Proactive Invocation**: The AI can now autonomously trigger mind map generation based on the project's logic and user intent without manual clicks. +- **Native Rich UI Embedding**: Direct chat-stream HTML/iframe rendering with smooth interaction and responsive scaling. +- **Smart Context Injection**: Completely refactored to use native `__messages__`. Simplifies calling parameters while ensuring deep understanding of the conversation history. +- **Fluid Responsive Layout**: Implemented golden-ratio `aspect-ratio: 16 / 9` with flexible height bounds for a consistent experience across Desktop and Mobile. +- **Deep i18n Support**: 4-level automated language detection cascade (JS > Header > Profile > Default). Natively supports 13 languages: **English, Simplified Chinese, Traditional Chinese (HK/TW), Japanese, Korean, French, German, Spanish, Italian, Russian, Vietnamese, and Indonesian**. + +## Bug Fixes + +- **Iframe Background Bleed Fix**: Resolved the "white border" issue in dark mode by forcing bone-level transparency on the HTML/Body frame. +- **Drawing Area Refinement**: Replaced fixed 500px height with flexible clamping for better readability on varied screen sizes. + +## Migration & Setup + +- **Native Tool Calling**: For reliable autonomous triggering, MUST enable **"Native Tool Calling"** in either individual **Chat Settings** (Controls icon at top-right corner) or OpenWebUI Admin -> Model settings. +- **MESSAGE_COUNT**: Defaulting to `12` messages for optimal performance. Set to `0` for full conservation summaries. diff --git a/plugins/tools/smart-mind-map-tool/v1.0.0_CN.md b/plugins/tools/smart-mind-map-tool/v1.0.0_CN.md new file mode 100644 index 0000000..e0cfff0 --- /dev/null +++ b/plugins/tools/smart-mind-map-tool/v1.0.0_CN.md @@ -0,0 +1,23 @@ +# Smart Mind Map Tool v1.0.0 版本发布说明 + +## 概览 + +智能思维导图工具(Smart Mind Map Tool)作为广受好评的 Action 版本插件的深度进化,正式开启 v1.0.0 工具化时代。本次发布利用 OpenWebUI 0.8.0 的 Rich UI 技术,将“导图生成”从用户点按的“被动技能”转化为 AI 自主判断的“主动智能”,为您提供无缝的知识可视化体验。 + +## 新功能 + +- **AI 自主/主动调用**:AI 现在可以根据对话的复杂度和上下文,自动决定何时生成导图,无需用户手动触发。 +- **原生 Rich UI 嵌入**:得益于 OpenWebUI 最新架构,导图现在能够以 HTML/iframe 形式直接嵌入对话流中,支持实时缩放和交互。 +- **全自动上下文注入**:弃用了旧的手动文本输入参数,改为原生支持 `__messages__`。工具会自动从当前对话提取最近 12 条(默认)或全量消息进行结构化分析。 +- **完美响应式布局**:基于 16:9 黄金比例设计,并辅以 `aspect-ratio` 适配,解决移动端与桌面端的排版冲突问题。 +- **原生多语言支持**:支持 4 级自动化语言探测(JS 脚本 > 浏览器头 > 用户资料 > 默认回退)。内置适配了 13 种主流语言,包括:**简体中文、繁体中文(港/台)、英语、俄语、日语、韩语、法语、德语、西班牙语、意大利语、越南语、印度尼西亚语**。所有 UI 界面、按钮、状态提示及 AI 生成逻辑均经过本地化优化。 + +## 问题修复 + +- **背景融合黑洞修复**:解决了在暗黑模式下 iframe 边缘可能出现的白色背景露边问题,通过强制声明 HTML/Body 透明背景实现 UI 无缝融合。 +- **画布高度优化**:移除了固定 500px 高度,采用 `aspect-ratio` 和弹性高度限制,防止在小屏幕下内容被截断。 + +## 迁移与配置说明 + +- **Native Tool Calling**: 为了获得最佳体验,请务必在 **右上角对话设置**(参数图标)或 OpenWebUI 管理员设置的模型选项中开启 **“启用原生工具调用 (Native Tool Calling)”**。 +- **MESSAGE_COUNT**: 默认值为 `12`。如需分析整段超长会话,请在 Valves 中将其设为 `0`。