From 81279845e297dca10e581eeddc818ae4a6ceb71c Mon Sep 17 00:00:00 2001 From: fujie Date: Tue, 24 Feb 2026 22:19:48 +0800 Subject: [PATCH] chore(skills): sync .gemini and .github skills and fix i18n validator --- .gemini/skills/README.md | 44 ++++++++++ .../plugin-scaffolder/assets/template.py.j2 | 80 +++++++++++++++++++ .../plugin-scaffolder/scripts/scaffold.py | 57 +++++++++---- .github/skills/README.md | 44 ++++++++++ .github/skills/community-announcer/SKILL.md | 23 ++++++ .github/skills/doc-mirror-sync/SKILL.md | 14 ++++ .../skills/doc-mirror-sync/scripts/sync.py | 38 +++++++++ .github/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 +++++++ .github/skills/gh-issue-scheduler/SKILL.md | 42 ++++++++++ .../scripts/find_unanswered.sh | 42 ++++++++++ .github/skills/i18n-validator/SKILL.md | 14 ++++ .../i18n-validator/scripts/validate_i18n.py | 54 +++++++++++++ .github/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 +++++++++++++++ .github/skills/version-bumper/SKILL.md | 26 ++++++ .github/skills/version-bumper/scripts/bump.py | 70 ++++++++++++++++ 22 files changed, 954 insertions(+), 17 deletions(-) create mode 100644 .gemini/skills/README.md create mode 100644 .gemini/skills/plugin-scaffolder/assets/template.py.j2 create mode 100644 .github/skills/README.md create mode 100644 .github/skills/community-announcer/SKILL.md create mode 100644 .github/skills/doc-mirror-sync/SKILL.md create mode 100644 .github/skills/doc-mirror-sync/scripts/sync.py create mode 100644 .github/skills/gh-issue-replier/SKILL.md create mode 100644 .github/skills/gh-issue-replier/references/example_reference.md create mode 100644 .github/skills/gh-issue-replier/references/templates.md create mode 100755 .github/skills/gh-issue-replier/scripts/check_star.sh create mode 100644 .github/skills/gh-issue-scheduler/SKILL.md create mode 100755 .github/skills/gh-issue-scheduler/scripts/find_unanswered.sh create mode 100644 .github/skills/i18n-validator/SKILL.md create mode 100644 .github/skills/i18n-validator/scripts/validate_i18n.py create mode 100644 .github/skills/plugin-scaffolder/SKILL.md create mode 100644 .github/skills/plugin-scaffolder/assets/README_template.md create mode 100644 .github/skills/plugin-scaffolder/assets/template.py create mode 100644 .github/skills/plugin-scaffolder/assets/template.py.j2 create mode 100644 .github/skills/plugin-scaffolder/scripts/scaffold.py create mode 100644 .github/skills/version-bumper/SKILL.md create mode 100644 .github/skills/version-bumper/scripts/bump.py diff --git a/.gemini/skills/README.md b/.gemini/skills/README.md new file mode 100644 index 0000000..570ff03 --- /dev/null +++ b/.gemini/skills/README.md @@ -0,0 +1,44 @@ +# 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` + +## 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/.gemini/skills/plugin-scaffolder/assets/template.py.j2 b/.gemini/skills/plugin-scaffolder/assets/template.py.j2 new file mode 100644 index 0000000..3077c65 --- /dev/null +++ b/.gemini/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/.gemini/skills/plugin-scaffolder/scripts/scaffold.py b/.gemini/skills/plugin-scaffolder/scripts/scaffold.py index 6e1ccd5..09b9a49 100644 --- a/.gemini/skills/plugin-scaffolder/scripts/scaffold.py +++ b/.gemini/skills/plugin-scaffolder/scripts/scaffold.py @@ -2,40 +2,63 @@ 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 "Pipe" - method_name = "action" if p_type == "actions" else "outlet" if p_type == "filters" else "pipe" - + + 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 + "{{METHOD_NAME}}": method_name, } - + # Files to generate - templates = { - "assets/template.py": f"{p_name}.py", - "assets/README_template.md": "README.md", - "assets/README_template.md": "README_CN.md" # Simplified for now, in real use we'd have a CN template - } - + 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.items(): - with open(os.path.join(skill_root, t_path), 'r') as f: + + 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: + + 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>") diff --git a/.github/skills/README.md b/.github/skills/README.md new file mode 100644 index 0000000..570ff03 --- /dev/null +++ b/.github/skills/README.md @@ -0,0 +1,44 @@ +# 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` + +## Notes + +- Skill definitions follow the expected location pattern: + - `.github/skills/<skill-name>/SKILL.md` +- Each skill may include optional `assets/`, `references/`, and `scripts/` folders. +- This directory mirrors `.gemini/skills` for compatibility. diff --git a/.github/skills/community-announcer/SKILL.md b/.github/skills/community-announcer/SKILL.md new file mode 100644 index 0000000..71261b8 --- /dev/null +++ b/.github/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/.github/skills/doc-mirror-sync/SKILL.md b/.github/skills/doc-mirror-sync/SKILL.md new file mode 100644 index 0000000..eabac26 --- /dev/null +++ b/.github/skills/doc-mirror-sync/SKILL.md @@ -0,0 +1,14 @@ +--- +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`. + +## Workflow +1. Identify changed READMEs. +2. Copy content to corresponding mirror paths. +3. Update version badges in `docs/plugins/{type}/index.md`. diff --git a/.github/skills/doc-mirror-sync/scripts/sync.py b/.github/skills/doc-mirror-sync/scripts/sync.py new file mode 100644 index 0000000..508674a --- /dev/null +++ b/.github/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/.github/skills/gh-issue-replier/SKILL.md b/.github/skills/gh-issue-replier/SKILL.md new file mode 100644 index 0000000..0873575 --- /dev/null +++ b/.github/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 <issue-number>` + * 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 <issue-number> +``` + +### Post Comment +```bash +gh issue comment <issue-number> --body "<message-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 <number>`. diff --git a/.github/skills/gh-issue-replier/references/example_reference.md b/.github/skills/gh-issue-replier/references/example_reference.md new file mode 100644 index 0000000..2bc2bd5 --- /dev/null +++ b/.github/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/.github/skills/gh-issue-replier/references/templates.md b/.github/skills/gh-issue-replier/references/templates.md new file mode 100644 index 0000000..2aa7438 --- /dev/null +++ b/.github/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/.github/skills/gh-issue-replier/scripts/check_star.sh b/.github/skills/gh-issue-replier/scripts/check_star.sh new file mode 100755 index 0000000..16b0a23 --- /dev/null +++ b/.github/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_number> + +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/.github/skills/gh-issue-scheduler/SKILL.md b/.github/skills/gh-issue-scheduler/SKILL.md new file mode 100644 index 0000000..b0ac8db --- /dev/null +++ b/.github/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/.github/skills/gh-issue-scheduler/scripts/find_unanswered.sh b/.github/skills/gh-issue-scheduler/scripts/find_unanswered.sh new file mode 100755 index 0000000..a423feb --- /dev/null +++ b/.github/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/.github/skills/i18n-validator/SKILL.md b/.github/skills/i18n-validator/SKILL.md new file mode 100644 index 0000000..5b302de --- /dev/null +++ b/.github/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/.github/skills/i18n-validator/scripts/validate_i18n.py b/.github/skills/i18n-validator/scripts/validate_i18n.py new file mode 100644 index 0000000..8d04d48 --- /dev/null +++ b/.github/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 <path_to_plugin.py>") + sys.exit(1) + check_i18n(sys.argv[1]) diff --git a/.github/skills/plugin-scaffolder/SKILL.md b/.github/skills/plugin-scaffolder/SKILL.md new file mode 100644 index 0000000..00bd8e3 --- /dev/null +++ b/.github/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/.github/skills/plugin-scaffolder/assets/README_template.md b/.github/skills/plugin-scaffolder/assets/README_template.md new file mode 100644 index 0000000..17d93e7 --- /dev/null +++ b/.github/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/.github/skills/plugin-scaffolder/assets/template.py b/.github/skills/plugin-scaffolder/assets/template.py new file mode 100644 index 0000000..3077c65 --- /dev/null +++ b/.github/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/.github/skills/plugin-scaffolder/assets/template.py.j2 b/.github/skills/plugin-scaffolder/assets/template.py.j2 new file mode 100644 index 0000000..3077c65 --- /dev/null +++ b/.github/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/.github/skills/plugin-scaffolder/scripts/scaffold.py b/.github/skills/plugin-scaffolder/scripts/scaffold.py new file mode 100644 index 0000000..09b9a49 --- /dev/null +++ b/.github/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 <type> <name> <title> <desc>") + sys.exit(1) + scaffold(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) diff --git a/.github/skills/version-bumper/SKILL.md b/.github/skills/version-bumper/SKILL.md new file mode 100644 index 0000000..02b5d2c --- /dev/null +++ b/.github/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/.github/skills/version-bumper/scripts/bump.py b/.github/skills/version-bumper/scripts/bump.py new file mode 100644 index 0000000..c2c9ffe --- /dev/null +++ b/.github/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])