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()