From 6d7a5b45cf5c53d091d33787ebdd8ffb8e12d06a Mon Sep 17 00:00:00 2001 From: fujie Date: Thu, 8 Jan 2026 00:10:47 +0800 Subject: [PATCH] feat: bump export_to_word to v0.4.3 and automate plugin publishing --- .github/workflows/publish_plugin.yml | 28 ++ docs/plugins/actions/export-to-word.md | 5 +- docs/plugins/actions/export-to-word.zh.md | 4 +- plugins/actions/export_to_docx/README.md | 6 +- plugins/actions/export_to_docx/README_CN.md | 6 +- .../actions/export_to_docx/export_to_word.py | 3 +- .../export_to_docx/export_to_word_cn.py | 3 +- scripts/fetch_remote_versions.py | 50 ++++ scripts/publish_plugin.py | 262 ++++++++++++++++++ scripts/sync_plugin_ids.py | 132 +++++++++ 10 files changed, 488 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/publish_plugin.yml create mode 100644 scripts/fetch_remote_versions.py create mode 100644 scripts/publish_plugin.py create mode 100644 scripts/sync_plugin_ids.py diff --git a/.github/workflows/publish_plugin.yml b/.github/workflows/publish_plugin.yml new file mode 100644 index 0000000..3233dcd --- /dev/null +++ b/.github/workflows/publish_plugin.yml @@ -0,0 +1,28 @@ +name: Publish Plugins to OpenWebUI Market + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install requests + + - name: Publish Plugins + env: + OPENWEBUI_API_KEY: ${{ secrets.OPENWEBUI_API_KEY }} + run: python scripts/publish_plugin.py diff --git a/docs/plugins/actions/export-to-word.md b/docs/plugins/actions/export-to-word.md index 66cacdb..6daa969 100644 --- a/docs/plugins/actions/export-to-word.md +++ b/docs/plugins/actions/export-to-word.md @@ -1,7 +1,7 @@ # Export to Word Action -v0.4.2 +v0.4.3 Export conversation to Word (.docx) with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**. @@ -53,6 +53,8 @@ You can configure the following settings via the **Valves** button in the plugin | `MATH_ENABLE` | Enable LaTeX math block conversion. | `True` | | `MATH_INLINE_DOLLAR_ENABLE` | Enable inline `$ ... $` math conversion. | `True` | +## 🔥 What's New in v0.4.3 + ### User-Level Configuration (UserValves) Users can override the following settings in their personal settings: @@ -118,3 +120,4 @@ Users can override the following settings in their personal settings: ## Source Code [:fontawesome-brands-github: View on GitHub](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/export_to_docx){ .md-button } +**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) diff --git a/docs/plugins/actions/export-to-word.zh.md b/docs/plugins/actions/export-to-word.zh.md index 8d0b7cc..643b122 100644 --- a/docs/plugins/actions/export-to-word.zh.md +++ b/docs/plugins/actions/export-to-word.zh.md @@ -1,7 +1,7 @@ # Export to Word(导出为 Word) Action -v0.4.2 +v0.4.3 将当前对话导出为完美格式的 Word 文档,支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用资料**以及**增强表格**渲染。 @@ -117,4 +117,4 @@ Export to Word 插件会把聊天消息从 Markdown 转成精致的 Word 文档 ## 源码 -[:fontawesome-brands-github: 在 GitHub 查看](https://github.com/Fu-Jie/awesome-openwebui/tree/main/plugins/actions/export_to_docx){ .md-button } +[:fontawes**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui)/tree/main/plugins/actions/export_to_docx){ .md-button } diff --git a/plugins/actions/export_to_docx/README.md b/plugins/actions/export_to_docx/README.md index a115952..be2c79a 100644 --- a/plugins/actions/export_to_docx/README.md +++ b/plugins/actions/export_to_docx/README.md @@ -1,10 +1,10 @@ # 📝 Export to Word (Enhanced) -**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.2 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) +**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) Export conversation to Word (.docx) with **syntax highlighting**, **native math equations**, **Mermaid diagrams**, **citations**, and **enhanced table formatting**. -## 🔥 What's New in v0.4.2 +## 🔥 What's New in v0.4.3 - ✨ **S3 Object Storage Support**: Direct access to images stored in S3/MinIO via boto3, bypassing API layer for faster exports. - 🔧 **Multi-level File Fallback**: 6-level fallback mechanism for file retrieval (DB → S3 → Local → URL → API → Attributes). @@ -73,7 +73,7 @@ Export conversation to Word (.docx) with **syntax highlighting**, **native math ## 📝 Changelog -### v0.4.2 +### v0.4.3 - **S3 Object Storage**: Direct S3/MinIO access via boto3 for faster image retrieval. - **6-Level Fallback**: Robust file retrieval: DB → S3 → Local → URL → API → Attributes. - **Better Logging**: Improved error messages for debugging file access issues. diff --git a/plugins/actions/export_to_docx/README_CN.md b/plugins/actions/export_to_docx/README_CN.md index 4a8ceea..2ecb377 100644 --- a/plugins/actions/export_to_docx/README_CN.md +++ b/plugins/actions/export_to_docx/README_CN.md @@ -1,10 +1,10 @@ # 📝 导出为 Word (增强版) -**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.2 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) +**Author:** [Fu-Jie](https://github.com/Fu-Jie) | **Version:** 0.4.3 | **Project:** [Awesome OpenWebUI](https://github.com/Fu-Jie/awesome-openwebui) 将对话导出为 Word (.docx),支持**代码语法高亮**、**原生数学公式**、**Mermaid 图表**、**引用参考**和**增强表格格式**。 -## 🔥 v0.4.2 更新内容 +## 🔥 v0.4.3 更新内容 - ✨ **S3 对象存储支持**: 通过 boto3 直连 S3/MinIO,绕过 API 层,导出速度更快。 - 🔧 **多级文件回退**: 6 级文件获取机制(数据库 → S3 → 本地 → URL → API → 属性)。 @@ -73,7 +73,7 @@ ## 📝 更新日志 -### v0.4.2 +### v0.4.3 - **S3 对象存储**: 通过 boto3 直连 S3/MinIO,图片获取速度更快。 - **6 级回退机制**: 稳健的文件获取:数据库 → S3 → 本地 → URL → API → 属性。 - **日志优化**: 改进错误提示,便于调试文件访问问题。 diff --git a/plugins/actions/export_to_docx/export_to_word.py b/plugins/actions/export_to_docx/export_to_word.py index 7b606af..00f0804 100644 --- a/plugins/actions/export_to_docx/export_to_word.py +++ b/plugins/actions/export_to_docx/export_to_word.py @@ -3,7 +3,8 @@ title: Export to Word (Enhanced) author: Fu-Jie author_url: https://github.com/Fu-Jie funding_url: https://github.com/Fu-Jie/awesome-openwebui -version: 0.4.2 +version: 0.4.3 +openwebui_id: fca6a315-2a45-42cc-8c96-55cbc85f87f2 icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K requirements: python-docx, Pygments, latex2mathml, mathml2omml description: Export current conversation from Markdown to Word (.docx) with Mermaid diagrams rendered client-side (Mermaid.js, SVG+PNG), LaTeX math, real hyperlinks, improved tables, syntax highlighting, and blockquote support. diff --git a/plugins/actions/export_to_docx/export_to_word_cn.py b/plugins/actions/export_to_docx/export_to_word_cn.py index 0205a82..cd058df 100644 --- a/plugins/actions/export_to_docx/export_to_word_cn.py +++ b/plugins/actions/export_to_docx/export_to_word_cn.py @@ -3,7 +3,8 @@ title: 导出为 Word (增强版) author: Fu-Jie author_url: https://github.com/Fu-Jie funding_url: https://github.com/Fu-Jie/awesome-openwebui -version: 0.4.2 +version: 0.4.3 +openwebui_id: 8a6306c0-d005-4e46-aaae-8db3532c9ed5 icon_url: data:image/svg+xml;base64,PHN2ZwogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICB3aWR0aD0iMjQiCiAgaGVpZ2h0PSIyNCIKICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgZmlsbD0ibm9uZSIKICBzdHJva2U9ImN1cnJlbnRDb2xvciIKICBzdHJva2Utd2lkdGg9IjIiCiAgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgogIHN0cm9rZS1saW5lam9pbj0icm91bmQiCj4KICA8cGF0aCBkPSJNNiAyMmEyIDIgMCAwIDEtMi0yVjRhMiAyIDAgMCAxIDItMmg4YTIuNCAyLjQgMCAwIDEgMS43MDQuNzA2bDMuNTg4IDMuNTg4QTIuNCAyLjQgMCAwIDEgMjAgOHYxMmEyIDIgMCAwIDEtMiAyeiIgLz4KICA8cGF0aCBkPSJNMTQgMnY1YTEgMSAwIDAgMCAxIDFoNSIgLz4KICA8cGF0aCBkPSJNMTAgOUg4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxM0g4IiAvPgogIDxwYXRoIGQ9Ik0xNiAxN0g4IiAvPgo8L3N2Zz4K requirements: python-docx, Pygments, latex2mathml, mathml2omml description: 将对话导出为 Word (.docx),支持 Mermaid 图表 (客户端渲染 SVG+PNG)、LaTeX 数学公式、真实超链接、增强表格格式、代码高亮和引用块。 diff --git a/scripts/fetch_remote_versions.py b/scripts/fetch_remote_versions.py new file mode 100644 index 0000000..434b486 --- /dev/null +++ b/scripts/fetch_remote_versions.py @@ -0,0 +1,50 @@ +import json +import os +import sys + +# Add current directory to path +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +try: + from openwebui_stats import OpenWebUIStats +except ImportError: + print("Error: openwebui_stats.py not found.") + sys.exit(1) + + +def main(): + # Try to get token from env + token = os.environ.get("OPENWEBUI_API_KEY") + if not token: + print("Error: OPENWEBUI_API_KEY environment variable not set.") + sys.exit(1) + + print("Fetching remote plugins from OpenWebUI...") + client = OpenWebUIStats(token) + try: + posts = client.get_all_posts() + except Exception as e: + print(f"Error fetching posts: {e}") + sys.exit(1) + + formatted_plugins = [] + for post in posts: + # Save the full raw post object to ensure we have "compliant update json data" + # We inject a 'type' field just for the comparison script to know it's remote, + # but otherwise keep the structure identical to the API response. + post["type"] = "remote_plugin" + formatted_plugins.append(post) + + output_file = "remote_versions.json" + with open(output_file, "w", encoding="utf-8") as f: + json.dump(formatted_plugins, f, indent=2, ensure_ascii=False) + + print( + f"✅ Successfully saved {len(formatted_plugins)} remote plugins to {output_file}" + ) + print(f" You can now compare local vs remote using:") + print(f" python scripts/extract_plugin_versions.py --compare {output_file}") + + +if __name__ == "__main__": + main() diff --git a/scripts/publish_plugin.py b/scripts/publish_plugin.py new file mode 100644 index 0000000..a79df15 --- /dev/null +++ b/scripts/publish_plugin.py @@ -0,0 +1,262 @@ +import os +import sys +import json +import requests +import re + + +def parse_frontmatter(content): + """Extracts metadata from the python file docstring.""" + # Allow leading whitespace and handle potential shebangs + match = re.search(r'^\s*"""\n(.*?)\n"""', content, re.DOTALL) + if not match: + # Fallback for files starting with comments or shebangs + match = re.search(r'"""\n(.*?)\n"""', content, re.DOTALL) + if not match: + return {} + + frontmatter = match.group(1) + meta = {} + for line in frontmatter.split("\n"): + if ":" in line: + key, value = line.split(":", 1) + meta[key.strip()] = value.strip() + return meta + + +def sync_frontmatter(file_path, content, meta, post_data): + """Syncs remote metadata back to local file frontmatter.""" + changed = False + new_meta = meta.copy() + + # 1. Sync ID + if "openwebui_id" not in new_meta and "post_id" not in new_meta: + new_meta["openwebui_id"] = post_data.get("id") + changed = True + + # 2. Sync Icon URL (often set in UI) + manifest = ( + post_data.get("data", {}) + .get("function", {}) + .get("meta", {}) + .get("manifest", {}) + ) + if "icon_url" not in new_meta and manifest.get("icon_url"): + new_meta["icon_url"] = manifest.get("icon_url") + changed = True + + # 3. Sync other fields if missing locally + for field in ["author", "author_url", "funding_url"]: + if field not in new_meta and manifest.get(field): + new_meta[field] = manifest.get(field) + changed = True + + if changed: + print(f" Syncing metadata back to {os.path.basename(file_path)}...") + # Reconstruct frontmatter + # We need to replace the content inside the first """ ... """ + # This is a bit fragile with regex but sufficient for standard files + + def replacement(match): + lines = [] + # Keep existing description or comments if we can't parse them easily? + # Actually, let's just reconstruct the key-values we know + # and try to preserve the description if it was at the end + + # Simple approach: Rebuild the whole block based on new_meta + # This might lose comments inside the frontmatter, but standard format is simple keys + + # Try to preserve order: title, author, ..., version, ..., description + ordered_keys = [ + "title", + "author", + "author_url", + "funding_url", + "version", + "openwebui_id", + "icon_url", + "requirements", + "description", + ] + + block = ['"""'] + + # Add known keys in order + for k in ordered_keys: + if k in new_meta: + block.append(f"{k}: {new_meta[k]}") + + # Add any other custom keys + for k, v in new_meta.items(): + if k not in ordered_keys: + block.append(f"{k}: {v}") + + block.append('"""') + return "\n".join(block) + + new_content = re.sub( + r'^"""\n(.*?)\n"""', replacement, content, count=1, flags=re.DOTALL + ) + + # If regex didn't match (e.g. leading whitespace), try with whitespace + if new_content == content: + new_content = re.sub( + r'^\s*"""\n(.*?)\n"""', replacement, content, count=1, flags=re.DOTALL + ) + + if new_content != content: + with open(file_path, "w", encoding="utf-8") as f: + f.write(new_content) + return new_content # Return updated content + + return content + + +def update_plugin(file_path, post_id, token): + print(f"Processing {os.path.basename(file_path)} (ID: {post_id})...") + + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + + meta = parse_frontmatter(content) + if not meta: + print(f" Skipping: No frontmatter found.") + return False + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + # 1. Fetch existing post + try: + response = requests.get( + f"https://api.openwebui.com/api/v1/posts/{post_id}", headers=headers + ) + response.raise_for_status() + post_data = response.json() + except Exception as e: + print(f" Error fetching post: {e}") + return False + + # 1.5 Sync Metadata back to local file + try: + content = sync_frontmatter(file_path, content, meta, post_data) + # Re-parse meta in case it changed + meta = parse_frontmatter(content) + except Exception as e: + print(f" Warning: Failed to sync local metadata: {e}") + + # 2. Update ONLY Content and Manifest + try: + # Ensure structure exists before populating nested fields + if "data" not in post_data: + post_data["data"] = {} + if "function" not in post_data["data"]: + post_data["data"]["function"] = {} + if "meta" not in post_data["data"]["function"]: + post_data["data"]["function"]["meta"] = {} + if "manifest" not in post_data["data"]["function"]["meta"]: + post_data["data"]["function"]["meta"]["manifest"] = {} + + # Update 1: The Source Code (Inner Content) + post_data["data"]["function"]["content"] = content + + # Update 2: The Post Body/README (Outer Content) + # Try to find a matching README file + plugin_dir = os.path.dirname(file_path) + base_name = os.path.basename(file_path).lower() + readme_content = None + + # Determine preferred README filename + readme_files = [] + if base_name.endswith("_cn.py"): + readme_files = ["README_CN.md", "README.md"] + else: + readme_files = ["README.md", "README_CN.md"] + + for readme_name in readme_files: + readme_path = os.path.join(plugin_dir, readme_name) + if os.path.exists(readme_path): + try: + with open(readme_path, "r", encoding="utf-8") as f: + readme_content = f.read() + print(f" Using README: {readme_name}") + break + except Exception as e: + print(f" Error reading {readme_name}: {e}") + + if readme_content: + post_data["content"] = readme_content + elif "description" in meta: + post_data["content"] = meta["description"] + else: + post_data["content"] = "" + + # Update Manifest (Metadata) + post_data["data"]["function"]["meta"]["manifest"].update(meta) + + # Sync top-level fields for consistency + if "title" in meta: + post_data["title"] = meta["title"] + post_data["data"]["function"]["name"] = meta["title"] + if "description" in meta: + post_data["data"]["function"]["meta"]["description"] = meta["description"] + + except Exception as e: + print(f" Error preparing update: {e}") + return False + + # 3. Submit Update + try: + response = requests.post( + f"https://api.openwebui.com/api/v1/posts/{post_id}/update", + headers=headers, + json=post_data, + ) + response.raise_for_status() + print(f" ✅ Success!") + return True + except Exception as e: + print(f" ❌ Failed: {e}") + return False + + +def main(): + token = os.environ.get("OPENWEBUI_API_KEY") + if not token: + print("Error: OPENWEBUI_API_KEY not set.") + sys.exit(1) + + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + plugins_dir = os.path.join(base_dir, "plugins") + + count = 0 + # Walk through plugins directory + for root, _, files in os.walk(plugins_dir): + for file in files: + if file.endswith(".py"): + file_path = os.path.join(root, file) + + # Check for ID in file content without full parse first + with open(file_path, "r", encoding="utf-8") as f: + content = f.read( + 2000 + ) # Read first 2000 chars is enough for frontmatter + + # Simple regex to find ID + id_match = re.search( + r"(?:openwebui_id|post_id):\s*([a-z0-9-]+)", content + ) + + if id_match: + post_id = id_match.group(1).strip() + update_plugin(file_path, post_id, token) + count += 1 + + print(f"\nFinished. Updated {count} plugins.") + + +if __name__ == "__main__": + main() diff --git a/scripts/sync_plugin_ids.py b/scripts/sync_plugin_ids.py new file mode 100644 index 0000000..06e2654 --- /dev/null +++ b/scripts/sync_plugin_ids.py @@ -0,0 +1,132 @@ +import os +import sys +import re +import difflib + +# Add current directory to path +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +try: + from openwebui_stats import OpenWebUIStats + from extract_plugin_versions import scan_plugins_directory +except ImportError: + print("Error: Helper scripts not found.") + sys.exit(1) + + +def normalize(s): + if not s: + return "" + return re.sub(r"\s+", " ", s.lower().strip()) + + +def insert_id_into_file(file_path, post_id): + with open(file_path, "r", encoding="utf-8") as f: + lines = f.readlines() + + new_lines = [] + inserted = False + in_frontmatter = False + + for line in lines: + # Check for start/end of frontmatter + if line.strip() == '"""': + if not in_frontmatter: + in_frontmatter = True + else: + # End of frontmatter + in_frontmatter = False + + # Check if ID already exists + if in_frontmatter and ( + line.strip().startswith("openwebui_id:") + or line.strip().startswith("post_id:") + ): + print(f" ID already exists in {os.path.basename(file_path)}") + return False + + new_lines.append(line) + + # Insert after version + if in_frontmatter and not inserted and line.strip().startswith("version:"): + new_lines.append(f"openwebui_id: {post_id}\n") + inserted = True + + if inserted: + with open(file_path, "w", encoding="utf-8") as f: + f.writelines(new_lines) + return True + return False + + +def main(): + token = os.environ.get("OPENWEBUI_API_KEY") + if not token: + print("Error: OPENWEBUI_API_KEY environment variable not set.") + sys.exit(1) + + print("Fetching remote posts...") + client = OpenWebUIStats(token) + remote_posts = client.get_all_posts() + print(f"Fetched {len(remote_posts)} remote posts.") + + plugins_dir = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "plugins" + ) + local_plugins = scan_plugins_directory(plugins_dir) + print(f"Found {len(local_plugins)} local plugins.") + + matched_count = 0 + + for plugin in local_plugins: + local_title = plugin.get("title", "") + if not local_title: + continue + + file_path = plugin.get("file_path") + best_match = None + highest_ratio = 0.0 + + # 1. Try Exact Match on Manifest Title (High Confidence) + for post in remote_posts: + manifest_title = ( + post.get("data", {}) + .get("function", {}) + .get("meta", {}) + .get("manifest", {}) + .get("title") + ) + if manifest_title and normalize(manifest_title) == normalize(local_title): + best_match = post + highest_ratio = 1.0 + break + + # 2. Try Fuzzy Match on Post Title if no exact match + if not best_match: + for post in remote_posts: + post_title = post.get("title", "") + ratio = difflib.SequenceMatcher( + None, normalize(local_title), normalize(post_title) + ).ratio() + if ratio > 0.8 and ratio > highest_ratio: + highest_ratio = ratio + best_match = post + + if best_match: + post_id = best_match.get("id") + post_title = best_match.get("title") + print( + f"Match found: '{local_title}' <--> '{post_title}' (ID: {post_id}) [Score: {highest_ratio:.2f}]" + ) + + if insert_id_into_file(file_path, post_id): + print(f" -> Updated {os.path.basename(file_path)}") + matched_count += 1 + else: + print(f"No match found for: '{local_title}'") + + print(f"\nTotal updated: {matched_count}") + + +if __name__ == "__main__": + main()