From 59a1ddb854bf2bbcc2ee8f272fc5fae6ca068341 Mon Sep 17 00:00:00 2001 From: Alter-xyz <88554920+alterxyz@users.noreply.github.com> Date: Fri, 16 May 2025 23:51:43 +0800 Subject: [PATCH] chore: updates --- docs.json | 5 +- ...-third-party-signature-verification.zh.mdx | 131 +++++ .../0321-plugin-auto-publish-pr.zh.mdx | 328 +++++++++++ plugin_dev_zh/0321-release-overview.zh.mdx | 4 +- .../0431-example-overview-and-index.zh.mdx | 50 ++ plugin_dev_zh/0432-endpoint.zh.mdx | 2 +- ...imensions.py => 1_rename_by_dimensions.py} | 0 ...pply_docs_json.py => 2_apply_docs_json.py} | 549 +++++++++++------- ...g_in_page.py => 2_contributing_in_page.py} | 0 9 files changed, 854 insertions(+), 215 deletions(-) create mode 100644 plugin_dev_zh/0312-third-party-signature-verification.zh.mdx create mode 100644 plugin_dev_zh/0321-plugin-auto-publish-pr.zh.mdx create mode 100644 plugin_dev_zh/0431-example-overview-and-index.zh.mdx rename tools/{rename_by_dimensions.py => 1_rename_by_dimensions.py} (100%) rename tools/{apply_docs_json.py => 2_apply_docs_json.py} (53%) rename tools/{contributing_in_page.py => 2_contributing_in_page.py} (100%) 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 云端完成。 + +![Workflow](https://assets-docs.dify.ai/2025/04/60534de8e220f860947b32a8329a8349.png) + +**免费额度**: + +* 公共仓库:无限制 +* 私有仓库:每月 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) + + Create Secrets + + + 在仓库中创建 `.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` 字段 + +![Release](https://assets-docs.dify.ai/2025/04/9eed2b9110e91e18008b399e58198f03.png) + +3. 将所有更改推送到 main 分支 +4. 等待 GitHub Actions 自动完成打包、分支创建和 PR 提交 + +### 执行效果展示 + +当你推送代码到插件源码仓库的 main 分支时,GitHub Actions 将自动执行发布流程: + +* 自动打包插件为 `{plugin-name}-{version}.difypkg` 格式 +* 自动将打包文件推送到目标仓库 +* 自动创建 PR 合并到 fork 仓库 + +![Outcome](https://assets-docs.dify.ai/2025/04/60d5de910c6ce2482c67ddec3320311f.png) + +### 示例仓库 + +你可以参考[示例仓库](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