chore(skills): sync .gemini and .github skills and fix i18n validator

This commit is contained in:
fujie
2026-02-24 22:19:48 +08:00
parent 6b39531fbc
commit 81279845e2
22 changed files with 954 additions and 17 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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 <type> <name> <title> <desc>")
sys.exit(1)
scaffold(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])