diff --git a/docs.json b/docs.json
index 4b1b88a9..12601d6b 100644
--- a/docs.json
+++ b/docs.json
@@ -1140,12 +1140,14 @@
"group": "行为准则与规范",
"pages": [
"plugin_dev_zh/0312-contributor-covenant-code-of-conduct.zh",
- "plugin_dev_zh/0312-privacy-protection-guidelines.zh"
+ "plugin_dev_zh/0312-privacy-protection-guidelines.zh",
+ "plugin_dev_zh/0312-third-party-signature-verification.zh"
]
},
{
"group": "发布与上架",
"pages": [
+ "plugin_dev_zh/0321-plugin-auto-publish-pr.zh",
"plugin_dev_zh/0321-release-overview.zh",
"plugin_dev_zh/0322-release-by-file.zh",
"plugin_dev_zh/0322-release-to-dify-marketplace.zh",
@@ -1166,6 +1168,7 @@
{
"group": "开发示例",
"pages": [
+ "plugin_dev_zh/0431-example-overview-and-index.zh",
"plugin_dev_zh/0432-develop-a-slack-bot-plugin.zh",
"plugin_dev_zh/0432-endpoint.zh"
]
diff --git a/plugin_dev_zh/0312-third-party-signature-verification.zh.mdx b/plugin_dev_zh/0312-third-party-signature-verification.zh.mdx
new file mode 100644
index 00000000..6ad73f5c
--- /dev/null
+++ b/plugin_dev_zh/0312-third-party-signature-verification.zh.mdx
@@ -0,0 +1,131 @@
+---
+dimensions:
+ type:
+ primary: operational
+ detail: setup
+ level: intermediate
+standard_title: Third-Party Signature Verification
+language: zh
+title: 第三方签名验证
+description: 本文档介绍了如何在Dify社区版中启用和使用第三方签名验证功能,包括密钥对生成、插件签名与验证、以及环境配置步骤,使管理员能够安全地安装未在Dify市场上提供的插件。
+---
+
+
+此功能仅在 Dify 社区版中可用,Dify 云端版目前不支持第三方签名验证。
+
+
+第三方签名验证允许 Dify 管理员安全地安装尚未在 Dify 市场上提供的插件,而无需修改 `.env` 文件绕开签名验证安全限制。
+
+**使用场景:**
+
+* 一经同意,Dify 管理员可以为插件添加由开发者发送的签名。
+* 插件开发者可以为他们的插件添加签名,并将其与公钥一起发布给无法禁用签名验证的 Dify 管理员。
+
+Dify 管理员和插件开发者都可以使用预先生成的密钥对为插件添加签名。此外,管理员可以配置 Dify 在插件安装过程中使用特定公钥强制执行签名验证。
+
+## 生成用于签名和验证的密钥对
+
+使用以下命令生成新的密钥对,用于添加和验证插件签名:
+
+```bash
+dify signature generate -f your_key_pair
+```
+
+运行此命令后,将在当前目录中生成两个文件:
+
+* **私钥**:`your_key_pair.private.pem`
+* **公钥**:`your_key_pair.public.pem`
+
+私钥用于签名插件,公钥用于验证插件的签名。
+
+请妥善保管私钥。如果私钥被泄露,攻击者可以为任何插件添加有效签名,这将危及 Dify 社区版的安全性。
+
+## 为插件添加签名并验证
+
+运行以下命令为插件添加签名。请注意,你需要指定**待签名的插件文件**和**私钥**:
+
+```bash
+dify signature sign your_plugin_project.difypkg -p your_key_pair.private.pem
+```
+
+执行命令后,将在同一目录中生成一个新的插件文件,文件名中添加了 `signed`:`your_plugin_project.signed.difypkg`
+
+你可以使用以下命令验证插件是否已正确签名。运行命令时需要指定**已签名的插件文件**和**公钥**:
+
+```bash
+dify signature verify your_plugin_project.signed.difypkg -p your_key_pair.public.pem
+```
+
+如果省略公钥参数,验证将使用 Dify 市场公钥。在这种情况下,对于任何未从 Dify 市场下载的插件文件,签名验证将失败。
+
+## 启用第三方签名验证
+
+Dify 管理员可以在安装插件前强制使用预先批准的公钥进行签名验证。
+
+### 放置公钥
+
+将用于签名的私钥对应的**公钥**放在插件守护程序可以访问的位置。
+
+例如,在 `docker/volumes/plugin_daemon` 下创建 `public_keys` 目录,并将公钥文件复制到对应路径:
+
+```bash
+mkdir docker/volumes/plugin_daemon/public_keys
+cp your_key_pair.public.pem docker/volumes/plugin_daemon/public_keys
+```
+
+### 环境变量配置
+
+在 `plugin_daemon` 容器中,配置以下环境变量:
+
+* `THIRD_PARTY_SIGNATURE_VERIFICATION_ENABLED`
+ * 启用第三方签名验证。
+ * 将此项设置为 `true` 以启用此功能。
+* `THIRD_PARTY_SIGNATURE_VERIFICATION_PUBLIC_KEYS`
+ * 指定用于签名验证的公钥文件路径。
+ * 你可以列出多个公钥文件,用逗号分隔。
+
+以下是配置这些变量的 Docker Compose 覆盖文件 (`docker-compose.override.yaml`) 示例:
+
+```yaml
+services:
+ plugin_daemon:
+ environment:
+ FORCE_VERIFYING_SIGNATURE: true
+ THIRD_PARTY_SIGNATURE_VERIFICATION_ENABLED: true
+ THIRD_PARTY_SIGNATURE_VERIFICATION_PUBLIC_KEYS: /app/storage/public_keys/your_key_pair.public.pem
+```
+
+请注意,`docker/volumes/plugin_daemon` 在 `plugin_daemon` 容器中被挂载到 `/app/storage`。确保在 `THIRD_PARTY_SIGNATURE_VERIFICATION_PUBLIC_KEYS` 中指定的路径对应于容器内的路径。
+
+要应用这些更改,请重启 Dify 服务:
+
+```bash
+cd docker
+docker compose down
+docker compose up -d
+```
+
+重启服务后,第三方签名验证功能将在当前社区版环境中启用。
+
+{/*
+Contributing Section
+DO NOT edit this section!
+It will be automatically generated by the script.
+*/}
+
+
+
+ 通过直接提交修改来帮助改进文档内容
+
+
+ 发现错误或有改进建议?请提交问题反馈
+
+
diff --git a/plugin_dev_zh/0321-plugin-auto-publish-pr.zh.mdx b/plugin_dev_zh/0321-plugin-auto-publish-pr.zh.mdx
new file mode 100644
index 00000000..73351a3b
--- /dev/null
+++ b/plugin_dev_zh/0321-plugin-auto-publish-pr.zh.mdx
@@ -0,0 +1,328 @@
+---
+dimensions:
+ type:
+ primary: operational
+ detail: deployment
+ level: beginner
+standard_title: Plugin Auto Publish PR
+language: zh
+title: 自动发布插件
+description: 本文档介绍了如何使用GitHub Actions自动化Dify插件的发布流程,包括配置步骤、参数说明及使用方法,帮助插件开发者简化发布更新流程,无需手动
+---
+
+### 背景
+
+当插件贡献者需要严肃更新 **已被其他用户使用的 Dify 插件** 时,流程通常非常繁琐:贡献者需要先修改插件源码并更新版本号,将更改推送到插件源码仓库,并在 fork 的 dify-plugin 仓库中创建新分支。随后,贡献者需要手动打包插件并上传打包文件,再创建 PR 合并到原始 dify-plugin 仓库。这个过程必须在每次插件代码变更时重复,非常耗时低效。
+
+为了简化这一流程,我们构建了基于 GitHub Actions 的自动化工作流 **Plugin Auto-PR**。借助这个工具,插件贡献者可以一键完成插件打包、分支推送以及 PR 创建。
+
+### 概念简介
+
+#### GitHub Actions
+
+GitHub Actions 是 GitHub 提供的内置 CI/CD 服务,可以自动化各种构建、测试和部署任务。
+
+**运行原理**:当触发条件(如 push 代码)满足时,GitHub 会自动分配虚拟机运行你的工作流。所有操作都在 GitHub 云端完成。
+
+
+
+**免费额度**:
+
+* 公共仓库:无限制
+* 私有仓库:每月 2000 分钟
+
+#### Plugin Auto-PR
+
+**运行原理**:
+
+1. 当你推送代码到插件源码仓库的 main 分支时,触发工作流
+2. 工作流从 `manifest.yaml` 文件中读取插件信息
+3. 自动打包插件为 `.difypkg` 文件
+4. 将打包文件推送到你 fork 的 `dify-plugins` 仓库中
+5. 创建新分支并提交更改
+6. 自动创建 PR 请求合并到上游仓库
+
+### 环境准备
+
+#### 仓库要求
+
+* 你已拥有自己的插件源码仓库(例如:`your-name/plugin-source`)
+* 你已拥有自己的 fork 插件仓库(例如:`your-name/dify-plugins`)
+* 你的 fork 仓库中已有插件目录结构:
+
+```
+dify-plugins/
+└── your-author-name
+ └── plugin-name
+```
+
+#### 权限要求
+
+此工作流需要适当的权限才能正常运行:
+
+* 你需要创建一个有足够权限的 GitHub Personal Access Token (PAT)
+* 该 PAT 必须有权向你的 fork 仓库推送代码
+* 该 PAT 必须有权向上游仓库创建 PR
+
+### 参数与配置项说明
+
+#### 必要参数
+
+插件自动发布工作流需要你正确配置以下关键元素:
+
+**manifest.yaml 文件**:这是整个自动化流程的核心配置源。你需要确保以下字段正确无误:
+
+* `name`:插件名称(将用于生成包名和分支名)
+* `version`:版本号(每次更新时需要递增)
+* `author`:GitHub 用户名(用于确定目标仓库路径)
+
+**PLUGIN_ACTION Secret**:你需要在插件源码仓库中正确设置此密钥。
+
+* 值要求:必须是具有足够权限的个人访问令牌(PAT)
+* 权限要求:能够推送分支到你的 fork 仓库,能够创建 PR 到上游仓库
+
+#### 自动生成的参数
+
+工作流会**自动处理以下内容**,无需手动干预:
+
+* GitHub 用户名:从 `manifest.yaml` 的 `author` 字段读取
+* 作者文件夹名称:与 `author` 字段保持一致
+* 插件名称:从 `manifest.yaml` 的 `name` 字段读取
+* 分支名称:`bump-{插件名}-plugin-{版本号}`
+* 打包文件名:`{插件名}-{版本号}.difypkg`
+* PR 标题和内容:基于插件名称和版本自动生成
+
+### 安装配置步骤
+
+
+
+ 确保你已经 fork 了官方的 `dify-plugins` 仓库,并且有自己的插件源码仓库。
+
+
+ 导航到你的插件源码仓库,点击 **Settings > Secrets and variables > Actions > New repository secret**,创建 GitHub Secret:
+
+ * 名称填写: `PLUGIN_ACTION`
+ * 值填写: 拥有目标仓库(`your-name/dify-plugins`)写入权限的 GitHub Personal Access Token (PAT)
+
+
+
+
+ 在仓库中创建 `.github/workflows/`目录,并在此目录中创建名为 `plugin-publish.yml` 的文件,再将以下内容复制到该文件中:
+
+ ```yaml
+ # .github/workflows/auto-pr.yml
+ name: Auto Create PR on Main Push
+
+ on:
+ push:
+ branches: [ main ] # Trigger on push to main
+
+ jobs:
+ create_pr: # Renamed job for clarity
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Print working directory # Kept for debugging
+ run: |
+ pwd
+ ls -la
+
+ - name: Download CLI tool
+ run: |
+ # Create bin directory in runner temp
+ mkdir -p $RUNNER_TEMP/bin
+ cd $RUNNER_TEMP/bin
+
+ # Download CLI tool
+ wget https://github.com/langgenius/dify-plugin-daemon/releases/download/0.0.6/dify-plugin-linux-amd64
+ chmod +x dify-plugin-linux-amd64
+
+ # Show download location and file
+ echo "CLI tool location:"
+ pwd
+ ls -la dify-plugin-linux-amd64
+
+ - name: Get basic info from manifest # Changed step name and content
+ id: get_basic_info
+ run: |
+ PLUGIN_NAME=$(grep "^name:" manifest.yaml | cut -d' ' -f2)
+ echo "Plugin name: $PLUGIN_NAME"
+ echo "plugin_name=$PLUGIN_NAME" >> $GITHUB_OUTPUT
+
+ VERSION=$(grep "^version:" manifest.yaml | cut -d' ' -f2)
+ echo "Plugin version: $VERSION"
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
+
+ # If the author's name is not your github username, you can change the author here
+ AUTHOR=$(grep "^author:" manifest.yaml | cut -d' ' -f2)
+ echo "Plugin author: $AUTHOR"
+ echo "author=$AUTHOR" >> $GITHUB_OUTPUT
+
+ - name: Package Plugin
+ id: package
+ run: |
+ # Use the downloaded CLI tool to package
+ cd $GITHUB_WORKSPACE
+ # Use variables for package name
+ PACKAGE_NAME="${{ steps.get_basic_info.outputs.plugin_name }}-${{ steps.get_basic_info.outputs.version }}.difypkg"
+ # Use CLI from runner temp
+ $RUNNER_TEMP/bin/dify-plugin-linux-amd64 plugin package . -o "$PACKAGE_NAME"
+
+ # Show packaging result
+ echo "Package result:"
+ ls -la "$PACKAGE_NAME"
+ echo "package_name=$PACKAGE_NAME" >> $GITHUB_OUTPUT
+
+ # Show full file path and directory structure (kept for debugging)
+ echo "\\nFull file path:"
+ pwd
+ echo "\\nDirectory structure:"
+ tree || ls -R
+
+ - name: Checkout target repo
+ uses: actions/checkout@v3
+ with:
+ # Use author variable for repository
+ repository: ${{steps.get_basic_info.outputs.author}}/dify-plugins
+ path: dify-plugins
+ token: ${{ secrets.PLUGIN_ACTION }}
+ fetch-depth: 1 # Fetch only the last commit to speed up checkout
+ persist-credentials: true # Persist credentials for subsequent git operations
+
+ - name: Prepare and create PR
+ run: |
+ # Debug info (kept)
+ echo "Debug: Current directory $(pwd)"
+ # Use variable for package name
+ PACKAGE_NAME="${{ steps.get_basic_info.outputs.plugin_name }}-${{ steps.get_basic_info.outputs.version }}.difypkg"
+ echo "Debug: Package name: $PACKAGE_NAME"
+ ls -la
+
+ # Move the packaged file to the target directory using variables
+ mkdir -p dify-plugins/${{ steps.get_basic_info.outputs.author }}/${{ steps.get_basic_info.outputs.plugin_name }}
+ mv "$PACKAGE_NAME" dify-plugins/${{ steps.get_basic_info.outputs.author }}/${{ steps.get_basic_info.outputs.plugin_name }}/
+
+ # Enter the target repository directory
+ cd dify-plugins
+
+ # Configure git
+ git config user.name "GitHub Actions"
+ git config user.email "actions@github.com"
+
+ # Ensure we are on the latest main branch
+ git fetch origin main
+ git checkout main
+ git pull origin main
+
+ # Create and switch to a new branch using variables and new naming convention
+ BRANCH_NAME="bump-${{ steps.get_basic_info.outputs.plugin_name }}-plugin-${{ steps.get_basic_info.outputs.version }}"
+ git checkout -b "$BRANCH_NAME"
+
+ # Add and commit changes (using git add .)
+ git add .
+ git status # for debugging
+ # Use variables in commit message
+ git commit -m "bump ${{ steps.get_basic_info.outputs.plugin_name }} plugin to version ${{ steps.get_basic_info.outputs.version }}"
+
+ # Push to remote (use force just in case the branch existed before from a failed run)
+ git push -u origin "$BRANCH_NAME" --force
+
+ # Confirm branch has been pushed and wait for sync (GitHub API might need a moment)
+ git branch -a
+ echo "Waiting for branch to sync..."
+ sleep 10 # Wait 10 seconds for branch sync
+
+ - name: Create PR via GitHub API
+ env:
+ GH_TOKEN: ${{ secrets.PLUGIN_ACTION }} # Use the provided token for authentication
+ run: |
+ gh pr create \
+ --repo langgenius/dify-plugins \
+ --head "${{ steps.get_basic_info.outputs.author }}:${{ steps.get_basic_info.outputs.plugin_name }}-${{ steps.get_basic_info.outputs.version }}" \
+ --base main \
+ --title "bump ${{ steps.get_basic_info.outputs.plugin_name }} plugin to version ${{ steps.get_basic_info.outputs.version }}" \
+ --body "bump ${{ steps.get_basic_info.outputs.plugin_name }} plugin package to version ${{ steps.get_basic_info.outputs.version }}
+
+ Changes:
+ - Updated plugin package file" || echo "PR already exists or creation skipped." # Handle cases where PR already exists
+
+ - name: Print environment info # Kept for debugging
+ run: |
+ echo "GITHUB_WORKSPACE: $GITHUB_WORKSPACE"
+ echo "Current directory contents:"
+ ls -R
+ ```
+
+
+ 确保 `manifest.yaml` 文件正确设置以下字段:
+
+ ```yaml
+ version: 0.0.x # Version number
+ author: your-github-username # GitHub username/Author name
+ name: your-plugin-name # Plugin name
+ ```
+
+
+
+### 使用指南
+
+#### 初次使用流程
+
+首次设置自动发布工作流时,需要完成以下步骤:
+
+1. 确保你已经 fork 了官方的 `dify-plugins` 仓库
+2. 确保你的插件源码仓库结构正确
+3. 在插件源码仓库中设置 `PLUGIN_ACTION Secret`
+4. 创建工作流文件 `.github/workflows/plugin-publish.yml`
+5. 确保 `manifest.yaml` 文件中的 `name` 和 `author` 字段正确配置
+
+#### 后续更新流程
+
+设置完成后,每次需要发布新版本时,只需:
+
+1. 修改插件代码
+2. 更新 `manifest.yaml` 中的 `version` 字段
+
+
+
+3. 将所有更改推送到 main 分支
+4. 等待 GitHub Actions 自动完成打包、分支创建和 PR 提交
+
+### 执行效果展示
+
+当你推送代码到插件源码仓库的 main 分支时,GitHub Actions 将自动执行发布流程:
+
+* 自动打包插件为 `{plugin-name}-{version}.difypkg` 格式
+* 自动将打包文件推送到目标仓库
+* 自动创建 PR 合并到 fork 仓库
+
+
+
+### 示例仓库
+
+你可以参考[示例仓库](https://github.com/Yevanchen/exa-in-dify),了解完整的配置细节和最佳实践。
+
+{/*
+Contributing Section
+DO NOT edit this section!
+It will be automatically generated by the script.
+*/}
+
+
+
+ 通过直接提交修改来帮助改进文档内容
+
+
+ 发现错误或有改进建议?请提交问题反馈
+
+
diff --git a/plugin_dev_zh/0321-release-overview.zh.mdx b/plugin_dev_zh/0321-release-overview.zh.mdx
index aab18ba6..f2c7229a 100644
--- a/plugin_dev_zh/0321-release-overview.zh.mdx
+++ b/plugin_dev_zh/0321-release-overview.zh.mdx
@@ -76,11 +76,9 @@ description: 本文档介绍了Dify插件的三种发布方式:官方Marketpla
* **开源共享项目** → **推荐使用 GitHub**,方便版本管理与社区协作。
* **快速分发或内部测试** → **推荐使用插件文件**,简单高效地安装和分享。
-{/*
### 第三方验证
-安装非 Dify 市场内的插件时,有可能遇到第三方签名验证问题。解决办法请参考[第三方签名验证](./signing-plugins-for-third-party-signature-verification.mdx)。
-*/}
+安装非 Dify 市场内的插件时,有可能遇到第三方签名验证问题。解决办法请参考[第三方签名验证](/plugin_dev_zh/0312-third-party-signature-verification.zh)。
## 相关资源
diff --git a/plugin_dev_zh/0431-example-overview-and-index.zh.mdx b/plugin_dev_zh/0431-example-overview-and-index.zh.mdx
new file mode 100644
index 00000000..3aced731
--- /dev/null
+++ b/plugin_dev_zh/0431-example-overview-and-index.zh.mdx
@@ -0,0 +1,50 @@
+---
+dimensions:
+ type:
+ primary: reference
+ detail: examples
+ level: beginner
+standard_title: Example Overview and Index
+language: zh
+title: 开发示例目录
+---
+
+
+
+ tool-plugin
+
+
+ extension-plugin
+
+
+{/*
+Contributing Section
+DO NOT edit this section!
+It will be automatically generated by the script.
+*/}
+
+
+
+ 通过直接提交修改来帮助改进文档内容
+
+
+ 发现错误或有改进建议?请提交问题反馈
+
+
diff --git a/plugin_dev_zh/0432-endpoint.zh.mdx b/plugin_dev_zh/0432-endpoint.zh.mdx
index 4265764a..e4bd83d0 100644
--- a/plugin_dev_zh/0432-endpoint.zh.mdx
+++ b/plugin_dev_zh/0432-endpoint.zh.mdx
@@ -6,7 +6,7 @@ dimensions:
level: intermediate
standard_title: Endpoint
language: zh
-title: Endpoint
+title: 彩虹猫 Endpoint
description: Author Yeuoly,Allen 本文档详细介绍了Dify插件中Endpoint的结构和实现方式,以彩虹猫项目为例。内容包括如何定义Endpoint组、配置接口、实现_invoke方法以及处理请求和响应。文档详细解释了各种YAML配置字段的含义和使用方法。
---
diff --git a/tools/rename_by_dimensions.py b/tools/1_rename_by_dimensions.py
similarity index 100%
rename from tools/rename_by_dimensions.py
rename to tools/1_rename_by_dimensions.py
diff --git a/tools/apply_docs_json.py b/tools/2_apply_docs_json.py
similarity index 53%
rename from tools/apply_docs_json.py
rename to tools/2_apply_docs_json.py
index 3638ea89..92a40935 100644
--- a/tools/apply_docs_json.py
+++ b/tools/2_apply_docs_json.py
@@ -5,7 +5,7 @@ from collections import defaultdict
# --- 配置 ---
refresh = True # 如果为 True,将清空指定版本的 tabs
-DOCS_JSON_PATH = 'docs.json'
+DOCS_JSON_PATH = "docs.json"
# --- 简体中文配置(docs_config) ---
PLUGIN_DEV_ZH = {
@@ -13,30 +13,30 @@ PLUGIN_DEV_ZH = {
"LANGUAGE_CODE": "简体中文", # 注意:虽然变量名是 LANGUAGE_CODE,但会部署为 docs.json 中的 'version' 值。
"FILE_EXTENSION": ".zh.mdx",
"TARGET_TAB_NAME": "插件开发", # 新增:目标 Tab 名称
- "FILENAME_PATTERN": re.compile(r'^(\d{4})-(.*?)\.zh\.mdx$'), # 新增:文件名匹配模式
+ "FILENAME_PATTERN": re.compile(r"^(\d{4})-(.*?)\.zh\.mdx$"), # 新增:文件名匹配模式
"PWX_TO_GROUP_MAP": {
# --- PWX 到 Group 名称的映射 (统一到 "插件开发" Tab) ---
# (P, W, X) -> (tab_name, group_name, nested_group_name)
# Tab: 插件开发
# Group: 概念与入门
- ('0', '1', '1'): ("插件开发", "概念与入门", "概览"),
- ('0', '1', '3'): ("插件开发", "概念与入门", None),
+ ("0", "1", "1"): ("插件开发", "概念与入门", "概览"),
+ ("0", "1", "3"): ("插件开发", "概念与入门", None),
# Group: 开发实践
- ('0', '2', '1'): ("插件开发", "开发实践", "快速开始"),
- ('0', '2', '2'): ("插件开发", "开发实践", "开发 Dify 插件"),
+ ("0", "2", "1"): ("插件开发", "开发实践", "快速开始"),
+ ("0", "2", "2"): ("插件开发", "开发实践", "开发 Dify 插件"),
# Group: 贡献与发布
- ('0', '3', '1'): ("插件开发", "贡献与发布", "行为准则与规范"),
- ('0', '3', '2'): ("插件开发", "贡献与发布", "发布与上架"),
- ('0', '3', '3'): ("插件开发", "贡献与发布", "常见问题解答"),
+ ("0", "3", "1"): ("插件开发", "贡献与发布", "行为准则与规范"),
+ ("0", "3", "2"): ("插件开发", "贡献与发布", "发布与上架"),
+ ("0", "3", "3"): ("插件开发", "贡献与发布", "常见问题解答"),
# Group: 实践案例与示例
- ('0', '4', '3'): ("插件开发", "实践案例与示例", "开发示例"),
+ ("0", "4", "3"): ("插件开发", "实践案例与示例", "开发示例"),
# Group: 高级开发
- ('9', '2', '2'): ("插件开发", "高级开发", "Extension 与 Agent"),
- ('9', '2', '3'): ("插件开发", "高级开发", "Extension 与 Agent"),
- ('9', '4', '3'): ("插件开发", "高级开发", "Extension 与 Agent"),
- ('9', '2', '4'): ("插件开发", "高级开发", "反向调用"),
+ ("9", "2", "2"): ("插件开发", "高级开发", "Extension 与 Agent"),
+ ("9", "2", "3"): ("插件开发", "高级开发", "Extension 与 Agent"),
+ ("9", "4", "3"): ("插件开发", "高级开发", "Extension 与 Agent"),
+ ("9", "2", "4"): ("插件开发", "高级开发", "反向调用"),
# Group: Reference & Specifications
- ('0', '4', '1'): ("插件开发", "Reference & Specifications", "核心规范与功能"),
+ ("0", "4", "1"): ("插件开发", "Reference & Specifications", "核心规范与功能"),
},
"DESIRED_GROUP_ORDER": [
"概念与入门",
@@ -44,40 +44,81 @@ PLUGIN_DEV_ZH = {
"贡献与发布",
"实践案例与示例",
"高级开发",
- "Reference & Specifications" # 确保这个在最后
- ]
+ "Reference & Specifications", # 确保这个在最后
+ ],
}
# --- English Configuration ---
PLUGIN_DEV_EN = {
"DOCS_DIR": "plugin_dev_en", # Plugin development documentation directory
- "LANGUAGE_CODE": "English", # Note: Although the variable name is LANGUAGE_CODE, it will be deployed as the 'version' value in docs.json.
+ # Note: Although the variable name is LANGUAGE_CODE, it will be deployed as the 'version' value in docs.json.
+ "LANGUAGE_CODE": "English",
"FILE_EXTENSION": ".en.mdx",
"TARGET_TAB_NAME": "Plugin Development",
- "FILENAME_PATTERN": re.compile(r'^(\d{4})-(.*?)\.en\.mdx$'),
+ "FILENAME_PATTERN": re.compile(r"^(\d{4})-(.*?)\.en\.mdx$"),
"PWX_TO_GROUP_MAP": {
# --- PWX to Group Name Mapping (Unified under the "Plugin Development" Tab) ---
# (P, W, X) -> (tab_name, group_name, nested_group_name)
# Tab: Plugin Development
# Group: Concepts & Getting Started
- ('0', '1', '1'): ("Plugin Development", "Concepts & Getting Started", "Overview"),
- ('0', '1', '3'): ("Plugin Development", "Concepts & Getting Started", None),
+ ("0", "1", "1"): (
+ "Plugin Development",
+ "Concepts & Getting Started",
+ "Overview",
+ ),
+ ("0", "1", "3"): ("Plugin Development", "Concepts & Getting Started", None),
# Group: Development Practices
- ('0', '2', '1'): ("Plugin Development", "Development Practices", "Quick Start"),
- ('0', '2', '2'): ("Plugin Development", "Development Practices", "Developing Dify Plugins"),
+ ("0", "2", "1"): ("Plugin Development", "Development Practices", "Quick Start"),
+ ("0", "2", "2"): (
+ "Plugin Development",
+ "Development Practices",
+ "Developing Dify Plugins",
+ ),
# Group: Contribution & Publishing
- ('0', '3', '1'): ("Plugin Development", "Contribution & Publishing", "Code of Conduct & Standards"),
- ('0', '3', '2'): ("Plugin Development", "Contribution & Publishing", "Publishing & Listing"),
- ('0', '3', '3'): ("Plugin Development", "Contribution & Publishing", "FAQ"),
+ ("0", "3", "1"): (
+ "Plugin Development",
+ "Contribution & Publishing",
+ "Code of Conduct & Standards",
+ ),
+ ("0", "3", "2"): (
+ "Plugin Development",
+ "Contribution & Publishing",
+ "Publishing & Listing",
+ ),
+ ("0", "3", "3"): ("Plugin Development", "Contribution & Publishing", "FAQ"),
# Group: Examples & Use Cases
- ('0', '4', '3'): ("Plugin Development", "Examples & Use Cases", "Development Examples"),
+ ("0", "4", "3"): (
+ "Plugin Development",
+ "Examples & Use Cases",
+ "Development Examples",
+ ),
# Group: Advanced Development
- ('9', '2', '2'): ("Plugin Development", "Advanced Development", "Extension & Agent"),
- ('9', '2', '3'): ("Plugin Development", "Advanced Development", "Extension & Agent"),
- ('9', '4', '3'): ("Plugin Development", "Advanced Development", "Extension & Agent"),
- ('9', '2', '4'): ("Plugin Development", "Advanced Development", "Reverse Calling"),
+ ("9", "2", "2"): (
+ "Plugin Development",
+ "Advanced Development",
+ "Extension & Agent",
+ ),
+ ("9", "2", "3"): (
+ "Plugin Development",
+ "Advanced Development",
+ "Extension & Agent",
+ ),
+ ("9", "4", "3"): (
+ "Plugin Development",
+ "Advanced Development",
+ "Extension & Agent",
+ ),
+ ("9", "2", "4"): (
+ "Plugin Development",
+ "Advanced Development",
+ "Reverse Calling",
+ ),
# Group: Reference & Specifications
- ('0', '4', '1'): ("Plugin Development", "Reference & Specifications", "Core Specifications & Features"),
+ ("0", "4", "1"): (
+ "Plugin Development",
+ "Reference & Specifications",
+ "Core Specifications & Features",
+ ),
},
"DESIRED_GROUP_ORDER": [
"Concepts & Getting Started",
@@ -85,40 +126,46 @@ PLUGIN_DEV_EN = {
"Contribution & Publishing",
"Examples & Use Cases",
"Advanced Development",
- "Reference & Specifications" # Ensure this is last
- ]
+ "Reference & Specifications", # Ensure this is last
+ ],
}
# --- 日本語設定 (Japanese Configuration) ---
PLUGIN_DEV_JA = {
"DOCS_DIR": "plugin_dev_ja", # プラグイン開発ドキュメントディレクトリ
- "LANGUAGE_CODE": "日本語", #注意:変数名は LANGUAGE_CODE ですが、docs.json の 'version' 値としてデプロイされます。
+ "LANGUAGE_CODE": "日本語", # 注意:変数名は LANGUAGE_CODE ですが、docs.json の 'version' 値としてデプロイされます。
"FILE_EXTENSION": ".ja.mdx",
- "TARGET_TAB_NAME": "プラグイン開発", # 対象タブ名
- "FILENAME_PATTERN": re.compile(r'^(\d{4})-(.*?)\.ja\.mdx$'), # ファイル名照合パターン
+ "TARGET_TAB_NAME": "プラグイン開発", # 対象タブ名
+ "FILENAME_PATTERN": re.compile(
+ r"^(\d{4})-(.*?)\.ja\.mdx$"
+ ), # ファイル名照合パターン
"PWX_TO_GROUP_MAP": {
# --- PWX からグループ名へのマッピング(「プラグイン開発」タブに統一)---
# (P, W, X) -> (tab_name, group_name, nested_group_name)
# Tab: プラグイン開発
# Group: 概念と概要
- ('0', '1', '1'): ("プラグイン開発", "概念と概要", "概要"),
- ('0', '1', '3'): ("プラグイン開発", "概念と概要", None),
+ ("0", "1", "1"): ("プラグイン開発", "概念と概要", "概要"),
+ ("0", "1", "3"): ("プラグイン開発", "概念と概要", None),
# Group: 開発実践
- ('0', '2', '1'): ("プラグイン開発", "開発実践", "クイックスタート"),
- ('0', '2', '2'): ("プラグイン開発", "開発実践", "Difyプラグインの開発"),
+ ("0", "2", "1"): ("プラグイン開発", "開発実践", "クイックスタート"),
+ ("0", "2", "2"): ("プラグイン開発", "開発実践", "Difyプラグインの開発"),
# Group: 貢献と公開
- ('0', '3', '1'): ("プラグイン開発", "貢献と公開", "行動規範と基準"),
- ('0', '3', '2'): ("プラグイン開発", "貢献と公開", "公開と掲載"),
- ('0', '3', '3'): ("プラグイン開発", "貢献と公開", "よくある質問 (FAQ)"),
+ ("0", "3", "1"): ("プラグイン開発", "貢献と公開", "行動規範と基準"),
+ ("0", "3", "2"): ("プラグイン開発", "貢献と公開", "公開と掲載"),
+ ("0", "3", "3"): ("プラグイン開発", "貢献と公開", "よくある質問 (FAQ)"),
# Group: 実践例とユースケース
- ('0', '4', '3'): ("プラグイン開発", "実践例とユースケース", "開発例"),
+ ("0", "4", "3"): ("プラグイン開発", "実践例とユースケース", "開発例"),
# Group: 高度な開発
- ('9', '2', '2'): ("プラグイン開発", "高度な開発", "Extension と Agent"),
- ('9', '2', '3'): ("プラグイン開発", "高度な開発", "Extension と Agent"),
- ('9', '4', '3'): ("プラグイン開発", "高度な開発", "Extension と Agent"),
- ('9', '2', '4'): ("プラグイン開発", "高度な開発", "リバースコール"), # Reverse Calling
+ ("9", "2", "2"): ("プラグイン開発", "高度な開発", "Extension と Agent"),
+ ("9", "2", "3"): ("プラグイン開発", "高度な開発", "Extension と Agent"),
+ ("9", "4", "3"): ("プラグイン開発", "高度な開発", "Extension と Agent"),
+ ("9", "2", "4"): (
+ "プラグイン開発",
+ "高度な開発",
+ "リバースコール",
+ ), # Reverse Calling
# Group: リファレンスと仕様
- ('0', '4', '1'): ("プラグイン開発", "リファレンスと仕様", "コア仕様と機能"),
+ ("0", "4", "1"): ("プラグイン開発", "リファレンスと仕様", "コア仕様と機能"),
},
"DESIRED_GROUP_ORDER": [
"概念と概要",
@@ -126,47 +173,54 @@ PLUGIN_DEV_JA = {
"貢献と公開",
"実践例とユースケース",
"高度な開発",
- "リファレンスと仕様" # これが最後になるように確認
- ]
+ "リファレンスと仕様", # これが最後になるように確認
+ ],
}
# --- 辅助函数 ---
+
def clear_tabs_if_refresh(navigation_data, version_code, target_tab_name, do_refresh):
"""如果 do_refresh 为 True,则查找指定版本和目标 Tab,并清空其 groups 列表"""
if not do_refresh:
return False # 未执行清空
- if not navigation_data or 'versions' not in navigation_data:
+ if not navigation_data or "versions" not in navigation_data:
print("警告: 'navigation.versions' 未找到,无法清空 tabs。")
return False
version_found = False
tab_found_and_cleared = False
- for version_nav in navigation_data.get('versions', []):
- if version_nav.get('version') == version_code:
+ for version_nav in navigation_data.get("versions", []):
+ if version_nav.get("version") == version_code:
version_found = True
target_tab = None
- if 'tabs' in version_nav and isinstance(version_nav['tabs'], list):
- for tab in version_nav['tabs']:
- if isinstance(tab, dict) and tab.get('tab') == target_tab_name:
+ if "tabs" in version_nav and isinstance(version_nav["tabs"], list):
+ for tab in version_nav["tabs"]:
+ if isinstance(tab, dict) and tab.get("tab") == target_tab_name:
target_tab = tab
break
if target_tab:
- if 'groups' in target_tab:
- target_tab['groups'] = []
- print(f"信息: 已清空版本 '{version_code}' 下 Tab '{target_tab_name}' 的 groups (因为 refresh=True)。")
+ if "groups" in target_tab:
+ target_tab["groups"] = []
+ print(
+ f"信息: 已清空版本 '{version_code}' 下 Tab '{target_tab_name}' 的 groups (因为 refresh=True)。"
+ )
tab_found_and_cleared = True
else:
# 如果 'groups' 不存在,也视为一种“清空”状态,或者可以创建一个空的
- target_tab['groups'] = []
- print(f"信息: 版本 '{version_code}' 下 Tab '{target_tab_name}' 没有 'groups' 键,已确保其为空列表 (因为 refresh=True)。")
+ target_tab["groups"] = []
+ print(
+ f"信息: 版本 '{version_code}' 下 Tab '{target_tab_name}' 没有 'groups' 键,已确保其为空列表 (因为 refresh=True)。"
+ )
tab_found_and_cleared = True
else:
- print(f"警告: 在版本 '{version_code}' 中未找到目标 Tab '{target_tab_name}',无法清空其 groups。")
- break # 找到版本后即可退出循环
+ print(
+ f"警告: 在版本 '{version_code}' 中未找到目标 Tab '{target_tab_name}',无法清空其 groups。"
+ )
+ break # 找到版本后即可退出循环
if not version_found:
print(f"警告: 未找到版本 '{version_code}',无法清空任何 Tab。")
@@ -175,12 +229,14 @@ def clear_tabs_if_refresh(navigation_data, version_code, target_tab_name, do_ref
return tab_found_and_cleared
-def get_page_path(filename, docs_config): # docs_config 参数保留,但 FILE_EXTENSION 不再用于此处的后缀移除
+def get_page_path(
+ filename, docs_config
+): # docs_config 参数保留,但 FILE_EXTENSION 不再用于此处的后缀移除
"""从 mdx 文件名获取 mintlify 页面路径 (固定去掉末尾 .mdx 后缀)"""
docs_dir = docs_config["DOCS_DIR"]
# 固定移除末尾的 .mdx,以保留 .zh 或 .en 等语言标识
- if filename.endswith('.mdx'):
- base_filename = filename[:-len('.mdx')]
+ if filename.endswith(".mdx"):
+ base_filename = filename[: -len(".mdx")]
else:
# 如果不以 .mdx 结尾,则引发错误,因为这是预期格式
raise ValueError(f"错误: 文件名 '{filename}' 不以 '.mdx' 结尾,无法处理。")
@@ -192,40 +248,41 @@ def extract_existing_pages(navigation_data, version_code, target_tab_name):
"""递归提取指定版本和目标 Tab 下所有已存在的页面路径"""
existing_pages = set()
target_version_nav = None
- target_tab_nav = None # 新增:用于存储找到的目标 Tab 对象
+ target_tab_nav = None # 新增:用于存储找到的目标 Tab 对象
- if not navigation_data or 'versions' not in navigation_data:
+ if not navigation_data or "versions" not in navigation_data:
print("警告: 'navigation.versions' 未找到")
- return existing_pages, None, None # 返回三个值
+ return existing_pages, None, None # 返回三个值
# 查找目标版本
- for version_nav in navigation_data.get('versions', []):
- if version_nav.get('version') == version_code:
+ for version_nav in navigation_data.get("versions", []):
+ if version_nav.get("version") == version_code:
target_version_nav = version_nav
break
if not target_version_nav:
print(f"警告: 版本 '{version_code}' 在 docs.json 中未找到")
- return existing_pages, None, None # 返回三个值
+ return existing_pages, None, None # 返回三个值
# 在目标版本中查找目标 Tab
- if 'tabs' in target_version_nav and isinstance(target_version_nav['tabs'], list):
- for tab in target_version_nav['tabs']:
- if isinstance(tab, dict) and tab.get('tab') == target_tab_name:
- target_tab_nav = tab # 存储找到的 Tab 对象
+ if "tabs" in target_version_nav and isinstance(target_version_nav["tabs"], list):
+ for tab in target_version_nav["tabs"]:
+ if isinstance(tab, dict) and tab.get("tab") == target_tab_name:
+ target_tab_nav = tab # 存储找到的 Tab 对象
# 仅从目标 Tab 中提取页面
- for group in tab.get('groups', []):
+ for group in tab.get("groups", []):
if isinstance(group, dict):
_recursive_extract(group, existing_pages)
- break # 找到目标 Tab 后即可退出循环
- else: # 'tabs' might not exist or not be a list
- target_version_nav['tabs'] = []
-
+ break # 找到目标 Tab 后即可退出循环
+ else: # 'tabs' might not exist or not be a list
+ target_version_nav["tabs"] = []
if not target_tab_nav:
- print(f"警告: 在版本 '{version_code}' 中未找到 Tab '{target_tab_name}',无法提取现有页面。")
- # 即使 Tab 不存在,也返回版本导航对象,以便后续可能创建 Tab
- return existing_pages, target_version_nav, None
+ print(
+ f"警告: 在版本 '{version_code}' 中未找到 Tab '{target_tab_name}',无法提取现有页面。"
+ )
+ # 即使 Tab 不存在,也返回版本导航对象,以便后续可能创建 Tab
+ return existing_pages, target_version_nav, None
# 返回提取到的页面、版本导航对象和目标 Tab 对象
return existing_pages, target_version_nav, target_tab_nav
@@ -237,24 +294,24 @@ def _recursive_extract(group_item, pages_set):
if not isinstance(group_item, dict):
return
- if 'pages' in group_item and isinstance(group_item['pages'], list):
- for page in group_item['pages']:
+ if "pages" in group_item and isinstance(group_item["pages"], list):
+ for page in group_item["pages"]:
if isinstance(page, str):
pages_set.add(page)
- elif isinstance(page, dict) and 'group' in page:
+ elif isinstance(page, dict) and "group" in page:
# Recurse into nested groups
_recursive_extract(page, pages_set)
def remove_obsolete_pages(target_tab_data, pages_to_remove):
"""递归地从目标 Tab 的 groups 结构中移除失效页面路径。
- 注意:此函数直接修改传入的 target_tab_data 字典。
+ 注意:此函数直接修改传入的 target_tab_data 字典。
"""
- if not isinstance(target_tab_data, dict) or 'groups' not in target_tab_data:
+ if not isinstance(target_tab_data, dict) or "groups" not in target_tab_data:
# 如果输入不是预期的 Tab 结构,则直接返回
return
- groups = target_tab_data.get('groups', [])
+ groups = target_tab_data.get("groups", [])
if not isinstance(groups, list):
# 如果 groups 不是列表,也无法处理
return
@@ -268,19 +325,22 @@ def remove_obsolete_pages(target_tab_data, pages_to_remove):
_remove_obsolete_from_group(group_item, pages_to_remove)
# 如果处理后 group 的 pages 为空(且没有嵌套 group),可以选择移除该 group
# 当前逻辑:保留空 group 结构
- if not group_item.get('pages'):
- print(f"信息: Group '{group_item.get('group')}' 清理后为空,已保留结构。")
+ if not group_item.get("pages"):
+ print(
+ f"信息: Group '{group_item.get('group')}' 清理后为空,已保留结构。"
+ )
i += 1
else:
# 如果 groups 列表中包含非字典项(不符合预期),则跳过
i += 1
+
def _remove_obsolete_from_group(group_dict, pages_to_remove):
"""辅助函数,递归处理单个 group 或 nested group 内的 pages"""
- if not isinstance(group_dict, dict) or 'pages' not in group_dict:
+ if not isinstance(group_dict, dict) or "pages" not in group_dict:
return
- pages = group_dict.get('pages', [])
+ pages = group_dict.get("pages", [])
if not isinstance(pages, list):
return
@@ -291,92 +351,99 @@ def _remove_obsolete_from_group(group_dict, pages_to_remove):
new_pages.append(page_item)
else:
print(f" - {page_item} (从 Group '{group_dict.get('group')}' 移除)")
- elif isinstance(page_item, dict) and 'group' in page_item:
+ elif isinstance(page_item, dict) and "group" in page_item:
# 递归处理嵌套的 group
_remove_obsolete_from_group(page_item, pages_to_remove)
# 保留嵌套 group 结构,即使它变空
- if page_item or page_item.get('pages'): # 检查字典是否为空或 pages 是否存在
- new_pages.append(page_item)
+ if page_item or page_item.get("pages"): # 检查字典是否为空或 pages 是否存在
+ new_pages.append(page_item)
else:
- print(f"信息: 嵌套 Group '{page_item.get('group')}' 清理后为空,已保留结构。")
- new_pages.append(page_item) # 仍然添加空的嵌套组结构
+ print(
+ f"信息: 嵌套 Group '{page_item.get('group')}' 清理后为空,已保留结构。"
+ )
+ new_pages.append(page_item) # 仍然添加空的嵌套组结构
else:
# 保留无法识别的项
new_pages.append(page_item)
- group_dict['pages'] = new_pages
+ group_dict["pages"] = new_pages
-def find_or_create_target_group(target_version_nav, tab_name, group_name, nested_group_name):
+def find_or_create_target_group(
+ target_version_nav, tab_name, group_name, nested_group_name
+):
# 注意:target_version_nav 是特定版本对象,例如 {"version": "简体中文", "tabs": [...]}
target_tab = None
# Ensure 'tabs' exists and is a list
- if 'tabs' not in target_version_nav or not isinstance(target_version_nav['tabs'], list):
- target_version_nav['tabs'] = []
+ if "tabs" not in target_version_nav or not isinstance(
+ target_version_nav["tabs"], list
+ ):
+ target_version_nav["tabs"] = []
- for tab in target_version_nav['tabs']:
- if isinstance(tab, dict) and tab.get('tab') == tab_name:
+ for tab in target_version_nav["tabs"]:
+ if isinstance(tab, dict) and tab.get("tab") == tab_name:
target_tab = tab
break
if target_tab is None:
- target_tab = {'tab': tab_name, 'groups': []}
- target_version_nav['tabs'].append(target_tab)
+ target_tab = {"tab": tab_name, "groups": []}
+ target_version_nav["tabs"].append(target_tab)
target_group = None
# Ensure 'groups' exists and is a list
- if 'groups' not in target_tab or not isinstance(target_tab['groups'], list):
- target_tab['groups'] = []
+ if "groups" not in target_tab or not isinstance(target_tab["groups"], list):
+ target_tab["groups"] = []
- for group in target_tab['groups']:
- if isinstance(group, dict) and group.get('group') == group_name:
+ for group in target_tab["groups"]:
+ if isinstance(group, dict) and group.get("group") == group_name:
target_group = group
break
if target_group is None:
- target_group = {'group': group_name, 'pages': []}
- target_tab['groups'].append(target_group)
+ target_group = {"group": group_name, "pages": []}
+ target_tab["groups"].append(target_group)
# Ensure 'pages' exists in the target_group and is a list
- if 'pages' not in target_group or not isinstance(target_group['pages'], list):
- target_group['pages'] = []
+ if "pages" not in target_group or not isinstance(target_group["pages"], list):
+ target_group["pages"] = []
# Default container is the top-level group's pages list
- target_pages_container = target_group['pages']
+ target_pages_container = target_group["pages"]
if nested_group_name:
target_nested_group = None
# Find existing nested group
- for item in target_group['pages']:
- if isinstance(item, dict) and item.get('group') == nested_group_name:
+ for item in target_group["pages"]:
+ if isinstance(item, dict) and item.get("group") == nested_group_name:
target_nested_group = item
# Ensure pages list exists in nested group
- target_pages_container = target_nested_group.setdefault(
- 'pages', [])
+ target_pages_container = target_nested_group.setdefault("pages", [])
# Ensure it's actually a list after setdefault
if not isinstance(target_pages_container, list):
- target_nested_group['pages'] = []
- target_pages_container = target_nested_group['pages']
+ target_nested_group["pages"] = []
+ target_pages_container = target_nested_group["pages"]
break
# If not found, create it
if target_nested_group is None:
- target_nested_group = {'group': nested_group_name, 'pages': []}
+ target_nested_group = {"group": nested_group_name, "pages": []}
# Check if target_group['pages'] is already the container we want to add to
# This logic assumes nested groups are *always* dicts within the parent's 'pages' list
- target_group['pages'].append(target_nested_group)
- target_pages_container = target_nested_group['pages']
+ target_group["pages"].append(target_nested_group)
+ target_pages_container = target_nested_group["pages"]
# Final check before returning
if not isinstance(target_pages_container, list):
# 这表示内部逻辑错误,应该引发异常
raise RuntimeError(
- f"内部错误: 无法为 Tab='{tab_name}', Group='{group_name}', Nested='{nested_group_name}' 获取有效的 pages 列表。")
+ f"内部错误: 无法为 Tab='{tab_name}', Group='{group_name}', Nested='{nested_group_name}' 获取有效的 pages 列表。"
+ )
return target_pages_container
+
# --- 主逻辑 ---
def get_group_sort_key(group_dict, docs_config):
"""为排序提供 key,根据 DESIRED_GROUP_ORDER 返回索引,未知组放在最后"""
- group_name = group_dict.get('group', '')
+ group_name = group_dict.get("group", "")
desired_order = docs_config["DESIRED_GROUP_ORDER"]
try:
return desired_order.index(group_name)
@@ -384,9 +451,13 @@ def get_group_sort_key(group_dict, docs_config):
return len(desired_order) # 将未在列表中的组排在最后
-def main(docs_config, navigation_data): # navigation_data: 传入内存中的 navigation 字典供直接修改
+def main(
+ docs_config, navigation_data
+): # navigation_data: 传入内存中的 navigation 字典供直接修改
"""处理单个文档配置,并直接修改传入的 navigation_data"""
- print(f"\n--- 开始处理版本: {docs_config['LANGUAGE_CODE']} / Tab: {docs_config['TARGET_TAB_NAME']} ---")
+ print(
+ f"\n--- 开始处理版本: {docs_config['LANGUAGE_CODE']} / Tab: {docs_config['TARGET_TAB_NAME']} ---"
+ )
# 从 docs_config 获取配置值
language_code = docs_config["LANGUAGE_CODE"]
@@ -400,46 +471,57 @@ def main(docs_config, navigation_data): # navigation_data: 传入内存中的 n
navigation = navigation_data # 使用传入的 navigation 对象进行操作
# 使用 language_code 和 target_tab_name 清理目标 Tab
- was_refreshed = clear_tabs_if_refresh(navigation, language_code, target_tab_name, refresh)
+ was_refreshed = clear_tabs_if_refresh(
+ navigation, language_code, target_tab_name, refresh
+ )
if was_refreshed:
print(f"继续执行 Tab '{target_tab_name}' 的后续页面提取和添加操作...")
# 2. 提取目标 Tab 的现有页面或创建版本/Tab 导航
existing_pages, target_version_nav, target_tab_nav = extract_existing_pages(
- navigation, language_code, target_tab_name)
+ navigation, language_code, target_tab_name
+ )
if target_version_nav is None:
print(f"信息:在导航数据中未找到版本 '{language_code}',将创建。")
- if 'versions' not in navigation: # 确保 versions 列表存在
- navigation['versions'] = []
+ if "versions" not in navigation: # 确保 versions 列表存在
+ navigation["versions"] = []
target_version_nav = {"version": language_code, "tabs": []}
- navigation['versions'].append(target_version_nav)
+ navigation["versions"].append(target_version_nav)
existing_pages = set()
- target_tab_nav = None # 版本是新建的,Tab 肯定不存在
+ target_tab_nav = None # 版本是新建的,Tab 肯定不存在
# 如果目标 Tab 不存在,需要创建它
if target_tab_nav is None:
- print(f"信息: 在版本 '{language_code}' 中未找到 Tab '{target_tab_name}',将创建。")
- target_tab_nav = {'tab': target_tab_name, 'groups': []}
+ print(
+ f"信息: 在版本 '{language_code}' 中未找到 Tab '{target_tab_name}',将创建。"
+ )
+ target_tab_nav = {"tab": target_tab_name, "groups": []}
# 确保 target_version_nav['tabs'] 是列表
- if 'tabs' not in target_version_nav or not isinstance(target_version_nav['tabs'], list):
- target_version_nav['tabs'] = []
- target_version_nav['tabs'].append(target_tab_nav)
- existing_pages = set() # 新 Tab 没有现有页面
+ if "tabs" not in target_version_nav or not isinstance(
+ target_version_nav["tabs"], list
+ ):
+ target_version_nav["tabs"] = []
+ target_version_nav["tabs"].append(target_tab_nav)
+ existing_pages = set() # 新 Tab 没有现有页面
- print(f"找到 {len(existing_pages)} 个已存在的页面 (版本: '{language_code}', Tab: '{target_tab_name}')。")
+ print(
+ f"找到 {len(existing_pages)} 个已存在的页面 (版本: '{language_code}', Tab: '{target_tab_name}')。"
+ )
# 3. 扫描文件系统 (这部分不变,扫描目录下的所有匹配文件)
filesystem_pages = set()
valid_files = []
if not os.path.isdir(docs_dir):
# 如果目录不存在,则无法继续处理此配置,引发错误
- raise FileNotFoundError(f"错误: 配置 '{language_code}' 的文档目录 '{docs_dir}' 不存在。")
+ raise FileNotFoundError(
+ f"错误: 配置 '{language_code}' 的文档目录 '{docs_dir}' 不存在。"
+ )
else:
for filename in os.listdir(docs_dir):
# 使用配置中的 filename_pattern
if filename.endswith(file_extension) and filename_pattern.match(filename):
- try: # 添加 try-except 块以捕获 get_page_path 可能引发的 ValueError
+ try: # 添加 try-except 块以捕获 get_page_path 可能引发的 ValueError
page_path = get_page_path(filename, docs_config)
filesystem_pages.add(page_path)
valid_files.append(filename)
@@ -450,7 +532,6 @@ def main(docs_config, navigation_data): # navigation_data: 传入内存中的 n
# raise e
print(f"在 '{docs_dir}' 找到 {len(filesystem_pages)} 个有效的文档文件。")
-
# 4. 计算差异 (相对于目标 Tab 的 existing_pages)
new_files_paths = filesystem_pages - existing_pages
removed_files_paths = existing_pages - filesystem_pages
@@ -459,19 +540,23 @@ def main(docs_config, navigation_data): # navigation_data: 传入内存中的 n
print(f"移除文件数 (相对于 Tab '{target_tab_name}'): {len(removed_files_paths)}")
# 5. 移除失效页面 (仅从目标 Tab 移除)
- if removed_files_paths and target_tab_nav: # 确保目标 Tab 存在
+ if removed_files_paths and target_tab_nav: # 确保目标 Tab 存在
print(f"正在从 Tab '{target_tab_name}' 移除失效页面...")
- remove_obsolete_pages(target_tab_nav, removed_files_paths) # 直接传入目标 Tab 对象
+ remove_obsolete_pages(
+ target_tab_nav, removed_files_paths
+ ) # 直接传入目标 Tab 对象
print(f"已处理从 Tab '{target_tab_name}' 移除: {removed_files_paths}")
elif removed_files_paths:
- print(f"警告: 存在失效页面 {removed_files_paths},但未找到目标 Tab '{target_tab_name}' 进行移除。")
-
+ print(
+ f"警告: 存在失效页面 {removed_files_paths},但未找到目标 Tab '{target_tab_name}' 进行移除。"
+ )
# 6. 添加新页面 (逻辑不变,但 find_or_create_target_group 会确保添加到正确的 Tab 和 Group)
if new_files_paths:
print(f"正在向 Tab '{target_tab_name}' 添加新页面...")
new_files_sorted = sorted(
- [f for f in valid_files if get_page_path(f, docs_config) in new_files_paths])
+ [f for f in valid_files if get_page_path(f, docs_config) in new_files_paths]
+ )
groups_to_add = defaultdict(list)
for filename in new_files_sorted:
@@ -480,11 +565,13 @@ def main(docs_config, navigation_data): # navigation_data: 传入内存中的 n
pwxy = match.group(1)
if len(pwxy) >= 3:
p, w, x = pwxy[0], pwxy[1], pwxy[2]
- try: # 包裹 get_page_path 调用
+ try: # 包裹 get_page_path 调用
page_path = get_page_path(filename, docs_config)
except ValueError as e:
- print(f"错误处理文件 '{filename}' (添加阶段): {e}。将跳过此文件。")
- continue # 跳过这个文件
+ print(
+ f"错误处理文件 '{filename}' (添加阶段): {e}。将跳过此文件。"
+ )
+ continue # 跳过这个文件
group_key = (p, w, x)
if group_key in pwx_to_group_map:
@@ -492,39 +579,56 @@ def main(docs_config, navigation_data): # navigation_data: 传入内存中的 n
current_tab_name_from_map = map_result[0]
# 强制使用配置的目标 Tab 名称
if current_tab_name_from_map != target_tab_name:
- print(f"警告: 文件 '{filename}' 根据 PWX 映射到 Tab '{current_tab_name_from_map}',但当前配置强制处理 Tab '{target_tab_name}'。将添加到 '{target_tab_name}'。")
+ print(
+ f"警告: 文件 '{filename}' 根据 PWX 映射到 Tab '{current_tab_name_from_map}',但当前配置强制处理 Tab '{target_tab_name}'。将添加到 '{target_tab_name}'。"
+ )
# 始终使用配置中定义的 target_tab_name
tab_name_to_use = target_tab_name
if len(map_result) == 3:
_, group_name, nested_group_name = map_result
- else: # 兼容旧格式或只有两项的情况
+ else: # 兼容旧格式或只有两项的情况
if len(map_result) >= 2:
- _, group_name = map_result[:2] # 取前两项
+ _, group_name = map_result[:2] # 取前两项
else:
# 处理 map_result 项数不足的情况
- print(f"错误: PWX_TO_GROUP_MAP 中键 '{group_key}' 的值 '{map_result}' 格式不正确,至少需要两项。跳过文件 '{filename}'。")
+ print(
+ f"错误: PWX_TO_GROUP_MAP 中键 '{group_key}' 的值 '{map_result}' 格式不正确,至少需要两项。跳过文件 '{filename}'。"
+ )
continue
- nested_group_name = None # 假设没有嵌套组
+ nested_group_name = None # 假设没有嵌套组
- groups_to_add[(tab_name_to_use, group_name, nested_group_name)].append(
- page_path)
+ groups_to_add[
+ (tab_name_to_use, group_name, nested_group_name)
+ ].append(page_path)
else:
print(
- f"警告: 文件 '{filename}' 的 PWX 前缀 ('{p}', '{w}', '{x}') 在 PWX_TO_GROUP_MAP 中没有找到映射,将跳过添加。")
+ f"警告: 文件 '{filename}' 的 PWX 前缀 ('{p}', '{w}', '{x}') 在 PWX_TO_GROUP_MAP 中没有找到映射,将跳过添加。"
+ )
else:
# 数字前缀不足3位是文件名格式错误,应引发异常
raise ValueError(
- f"错误: 文件 '{filename}' 的数字前缀 '{pwxy}' 不足3位,无法解析 PWX。")
+ f"错误: 文件 '{filename}' 的数字前缀 '{pwxy}' 不足3位,无法解析 PWX。"
+ )
- for (tab_name, group_name, nested_group_name), pages_to_append in groups_to_add.items():
- # 确保只添加到目标 Tab 下 (此检查现在是多余的,因为上面强制使用了 target_tab_name)
+ for (
+ tab_name,
+ group_name,
+ nested_group_name,
+ ), pages_to_append in groups_to_add.items():
+ # 确保只添加到目标 Tab 下 (此检查现在是多余的,因为上面强制使用了 target_tab_name)
# if tab_name == target_tab_name:
print(
- f" 添加到 Tab='{tab_name}', Group='{group_name}', Nested='{nested_group_name or '[无]'}' : {len(pages_to_append)} 个页面")
+ f" 添加到 Tab='{tab_name}', Group='{group_name}', Nested='{nested_group_name or '[无]'}' : {len(pages_to_append)} 个页面"
+ )
# find_or_create_target_group 现在需要 target_version_nav 来定位或创建 Tab
target_pages_list = find_or_create_target_group(
- target_version_nav, tab_name, group_name, nested_group_name) # tab_name 此时应等于 target_tab_name
+ # tab_name 此时应等于 target_tab_name
+ target_version_nav,
+ tab_name,
+ group_name,
+ nested_group_name,
+ )
if isinstance(target_pages_list, list):
for new_page in pages_to_append:
@@ -534,44 +638,49 @@ def main(docs_config, navigation_data): # navigation_data: 传入内存中的 n
else:
# find_or_create_target_group 内部出错时会抛出 RuntimeError
# 这里可以加日志,但理论上不应到达
- print(f"错误: 未能为 Tab='{tab_name}', Group='{group_name}', Nested='{nested_group_name}' 获取有效的 pages 列表进行添加。")
+ print(
+ f"错误: 未能为 Tab='{tab_name}', Group='{group_name}', Nested='{nested_group_name}' 获取有效的 pages 列表进行添加。"
+ )
# else: # 这个 else 分支现在不会被触发
# print(f"信息: 跳过向非目标 Tab '{tab_name}' 添加页面 (目标 Tab: '{target_tab_name}')。")
-
# <-- 排序 Group (仅排序目标 Tab 内的 Group) -->
print(f"正在排序 Tab '{target_tab_name}' 内的 Group...")
- if target_tab_nav and 'groups' in target_tab_nav: # 确保目标 Tab 和 groups 存在
- groups_list = [g for g in target_tab_nav['groups'] if isinstance(g, dict)]
+ if target_tab_nav and "groups" in target_tab_nav: # 确保目标 Tab 和 groups 存在
+ groups_list = [g for g in target_tab_nav["groups"] if isinstance(g, dict)]
groups_list.sort(key=lambda g: get_group_sort_key(g, docs_config))
- target_tab_nav['groups'] = groups_list
+ target_tab_nav["groups"] = groups_list
print(f" 已对 Tab '{target_tab_name}' 中的 Group 进行排序。")
elif target_tab_nav:
- print(f" Tab '{target_tab_name}' 中没有 'groups' 或为空,无需排序。")
+ print(f" Tab '{target_tab_name}' 中没有 'groups' 或为空,无需排序。")
else:
- print(f" 未找到 Tab '{target_tab_name}',无法排序 Group。")
-
+ print(f" 未找到 Tab '{target_tab_name}',无法排序 Group。")
# 不再返回 docs_data,因为直接修改了传入的 navigation_data
- print(f"--- 完成处理版本: {docs_config['LANGUAGE_CODE']} / Tab: {docs_config['TARGET_TAB_NAME']} ---")
+ print(
+ f"--- 完成处理版本: {docs_config['LANGUAGE_CODE']} / Tab: {docs_config['TARGET_TAB_NAME']} ---"
+ )
def load_docs_data(path):
"""加载 JSON 文件,处理文件不存在和格式错误的情况"""
try:
- with open(path, 'r', encoding='utf-8') as f:
+ with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
print(f"信息: {path} 未找到,将创建新的结构。")
- return {"navigation": {"versions": []}} # 返回初始结构
+ return {"navigation": {"versions": []}} # 返回初始结构
except json.JSONDecodeError as e:
# 引发更具体的错误,而不是返回 None
- raise json.JSONDecodeError(f"错误: {path} 格式错误。无法继续。- {e.msg}", e.doc, e.pos)
+ raise json.JSONDecodeError(
+ f"错误: {path} 格式错误。无法继续。- {e.msg}", e.doc, e.pos
+ )
+
def save_docs_data(path, data):
"""保存 JSON 数据到文件"""
try:
- with open(path, 'w', encoding='utf-8') as f:
+ with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
print(f"\n成功更新 {path},包含所有已处理的版本。")
# 不再需要返回 True/False,因为异常会处理失败情况
@@ -582,59 +691,79 @@ def save_docs_data(path, data):
# 引发其他未知错误
raise Exception(f"写入 {path} 时发生未知错误: {e}")
+
def process_configurations(configs, docs_path):
"""加载数据,处理所有有效配置,然后保存数据"""
# 1. 加载初始数据
try:
current_docs_data = load_docs_data(docs_path)
except json.JSONDecodeError as e:
- print(e) # 打印加载错误信息
- return # 加载失败则退出
+ print(e) # 打印加载错误信息
+ return # 加载失败则退出
# current_docs_data 不会是 None,因为 load_docs_data 要么返回数据要么引发异常
# 2. 确保基本结构存在
- navigation_data = current_docs_data.setdefault('navigation', {}) # 获取 navigation 字典
- navigation_data.setdefault('versions', [])
+ navigation_data = current_docs_data.setdefault(
+ "navigation", {}
+ ) # 获取 navigation 字典
+ navigation_data.setdefault("versions", [])
# 3. 筛选有效配置
valid_configs = []
for config in configs:
- required_keys = ["DOCS_DIR", "LANGUAGE_CODE", "FILE_EXTENSION",
- "PWX_TO_GROUP_MAP", "DESIRED_GROUP_ORDER",
- "TARGET_TAB_NAME", "FILENAME_PATTERN"]
+ required_keys = [
+ "DOCS_DIR",
+ "LANGUAGE_CODE",
+ "FILE_EXTENSION",
+ "PWX_TO_GROUP_MAP",
+ "DESIRED_GROUP_ORDER",
+ "TARGET_TAB_NAME",
+ "FILENAME_PATTERN",
+ ]
if all(k in config for k in required_keys):
- # 可选:检查 PWX_TO_GROUP_MAP 和 DESIRED_GROUP_ORDER 是否为空
- # 并且检查 FILENAME_PATTERN 是否是编译后的正则表达式对象
- if (config.get("PWX_TO_GROUP_MAP") and
- config.get("DESIRED_GROUP_ORDER") and
- isinstance(config.get("FILENAME_PATTERN"), re.Pattern)):
- valid_configs.append(config)
- else:
- reason = []
- if not config.get("PWX_TO_GROUP_MAP"): reason.append("PWX_TO_GROUP_MAP 为空或不存在")
- if not config.get("DESIRED_GROUP_ORDER"): reason.append("DESIRED_GROUP_ORDER 为空或不存在")
- if not isinstance(config.get("FILENAME_PATTERN"), re.Pattern): reason.append("FILENAME_PATTERN 不是有效的正则表达式对象")
- print(f"警告: 配置 {config.get('LANGUAGE_CODE', '未知')} 无效 ({'; '.join(reason)}),跳过处理。")
+ # 可选:检查 PWX_TO_GROUP_MAP 和 DESIRED_GROUP_ORDER 是否为空
+ # 并且检查 FILENAME_PATTERN 是否是编译后的正则表达式对象
+ if (
+ config.get("PWX_TO_GROUP_MAP")
+ and config.get("DESIRED_GROUP_ORDER")
+ and isinstance(config.get("FILENAME_PATTERN"), re.Pattern)
+ ):
+ valid_configs.append(config)
+ else:
+ reason = []
+ if not config.get("PWX_TO_GROUP_MAP"):
+ reason.append("PWX_TO_GROUP_MAP 为空或不存在")
+ if not config.get("DESIRED_GROUP_ORDER"):
+ reason.append("DESIRED_GROUP_ORDER 为空或不存在")
+ if not isinstance(config.get("FILENAME_PATTERN"), re.Pattern):
+ reason.append("FILENAME_PATTERN 不是有效的正则表达式对象")
+ print(
+ f"警告: 配置 {config.get('LANGUAGE_CODE', '未知')} 无效 ({'; '.join(reason)}),跳过处理。"
+ )
else:
- missing_keys = [k for k in required_keys if k not in config]
- print(f"警告: 配置 {config.get('LANGUAGE_CODE', '未知')} 不完整 (缺少: {', '.join(missing_keys)}),跳过处理。")
+ missing_keys = [k for k in required_keys if k not in config]
+ print(
+ f"警告: 配置 {config.get('LANGUAGE_CODE', '未知')} 不完整 (缺少: {', '.join(missing_keys)}),跳过处理。"
+ )
# 4. 处理有效配置
if not valid_configs:
- print("没有有效的配置可供处理。")
+ print("没有有效的配置可供处理。")
else:
- try: # 包裹所有配置的处理过程
+ try: # 包裹所有配置的处理过程
for config in valid_configs:
# 将 navigation_data 传递给 main 函数进行修改
- main(config, navigation_data) # main 函数会直接修改这个 navigation_data 字典
+ # main 函数会直接修改这个 navigation_data 字典
+ main(config, navigation_data)
# 5. 所有配置处理完毕后,统一写回文件
save_docs_data(docs_path, current_docs_data)
except (FileNotFoundError, ValueError, RuntimeError, IOError, Exception) as e:
- # 捕获 main 或 save_docs_data 中可能引发的已知错误
- print(f"\n处理过程中发生错误: {e}")
- print("操作已终止,文件可能未完全更新。")
- # 根据需要,可以在这里决定是否尝试保存部分结果或直接退出
+ # 捕获 main 或 save_docs_data 中可能引发的已知错误
+ print(f"\n处理过程中发生错误: {e}")
+ print("操作已终止,文件可能未完全更新。")
+ # 根据需要,可以在这里决定是否尝试保存部分结果或直接退出
+
if __name__ == "__main__":
# 定义要处理的配置列表
@@ -645,4 +774,4 @@ if __name__ == "__main__":
]
# 调用主处理函数
- process_configurations(CONFIGS_TO_PROCESS, DOCS_JSON_PATH)
\ No newline at end of file
+ process_configurations(CONFIGS_TO_PROCESS, DOCS_JSON_PATH)
diff --git a/tools/contributing_in_page.py b/tools/2_contributing_in_page.py
similarity index 100%
rename from tools/contributing_in_page.py
rename to tools/2_contributing_in_page.py