在前后端协作中,接口文档经常出现三个问题:后端代码改了,文档没有同步;前端需要接口定义时,要反复复制粘贴;接口路径变更后,Apifox 中还会残留旧接口。

本文介绍一套可落地的协同方案:

后端代码
    ↓  编译 / 生成
openapi.json
    ↓
AI
    ├─ Apifox MCP:读取、查询接口文档
    └─ PowerShell 脚本:调用 Apifox Open API 回写
    ↓
Apifox:接口文档、Mock、调试和状态管理

这套方案的关键点是:MCP 负责“读”,脚本负责“写”,AI 负责编排整个流程。

一、整体架构

1. 后端代码

后端代码是接口实现的真实来源。接口完成开发后,需要先完成编译,并通过项目中的工具或插件生成最新的 OpenAPI 文件。

后端项目结构与编译入口

2. OpenAPI 文件

openapi.json 是后端接口的标准化描述文件,通常包含:

  • HTTP 请求方法;
  • 接口路径;
  • Query、Path、Header 和 Body 参数;
  • 请求体和响应结构;
  • 接口描述和 operationId
  • 接口目录、状态等扩展信息。

它是代码接口和 Apifox 文档之间的中间数据源。

3. Apifox

Apifox 是团队最终使用的接口协作平台,负责:

  • 保存接口文档;
  • 发布在线文档;
  • 提供 Mock;
  • 调试接口;
  • 供前端和 AI 查询接口定义;
  • 管理接口状态和废弃接口。

二、生成 OpenAPI 文件

首先使用 Apifox 插件或项目既有工具刷新接口,确认后端接口已经被识别。

Apifox 插件识别后端接口

然后选择导出 OpenAPI JSON:

导出 OpenAPI JSON

确认文件已经生成到项目约定目录:

项目目录中的 openapi.json

本文示例路径为:

E:\javaproject\plant-identifier\openapi.json

可以先验证 JSON 格式:

Get-Content -Raw -LiteralPath "E:\javaproject\plant-identifier\openapi.json" | ConvertFrom-Json | Out-Null
Write-Host "JSON OK"

注意:如果只修改了后端代码,却没有重新生成 openapi.json,后续脚本同步的仍然是旧接口定义。

三、配置 Apifox MCP

1. MCP 的作用

当前 Apifox MCP 主要负责读取和查询:

  • 读取 Apifox 项目的 OpenAPI 文档;
  • 查询项目接口列表;
  • 查询接口请求参数和响应结构;
  • 让 AI 了解 Apifox 中已有的接口;
  • 为前端开发和 AI 编码提供接口定义。

MCP 当前不负责把本地 openapi.json 直接写回 Apifox,回写动作由后面的 PowerShell 脚本完成。

2. 在 Trae 中添加 MCP

进入:

AI 侧栏 → 设置 → MCP → 添加 MCP Servers → 手动配置

Trae 设置中的 MCP 入口

MCP 页面中的手动配置入口

配置示例:

{
  "mcpServers": {
    "apifox": {
      "command": "npx",
      "args": [
        "-y",
        "apifox-mcp-server@latest",
        "--project=8702461"
      ],
      "env": {
        "APIFOX_ACCESS_TOKEN": "你的新 Token"
      }
    }
  }
}

其中:

配置项 作用
mcpServers MCP 服务配置根对象
apifox MCP 服务名称,可以自定义
command 使用 npx 启动 MCP 服务
apifox-mcp-server@latest Apifox MCP Server 包
--project=8702461 指定 Apifox 项目 ID
APIFOX_ACCESS_TOKEN Apifox API 访问令牌

3. 获取项目 ID和 Token

项目 ID 可以在 Apifox 项目设置的基本设置中查看:

Apifox 项目基本设置中的项目 ID

API Token 可以在账号设置中的 API 访问令牌页面创建:

Apifox 账号设置中的 API 访问令牌

Token 所属账号必须有目标项目的相应权限。需要注意:读取文档和导入回写所需的权限可能不同,执行导入时通常需要项目维护者或管理员权限。

安全建议:

  • 不要把真实 Token 提交到 Git;
  • 不要把 Token 写入博客、截图或聊天记录;
  • 团队协作时,建议使用本机环境变量;
  • Token 泄露后应立即删除并重新生成。

4. 验证 MCP

在 Trae 中对 AI 说:

请通过 Apifox MCP 获取当前项目的 API 文档,并告诉我项目中有多少个接口。

如果 AI 能够返回接口数量、目录或接口详情,说明 MCP 已连接。

四、配置 Apifox 回写脚本

1. 脚本的作用

apifox-import.ps1 负责完成 MCP 不负责的“写入”动作:

  • 读取本地 openapi.json
  • 调用 Apifox 的 import-openapi Open API;
  • 使用 AUTO_MERGE 创建或更新接口;
  • 同步接口目录;
  • 导出 Apifox 当前接口并与本地接口进行比对;
  • 找出本地已经不存在的旧接口;
  • 将旧接口标记为 deprecated
  • 输出创建、更新、废弃和失败数量。

2. 脚本文件和参数

项目中的 apifox-import.ps1 脚本

示例配置:

脚本参数与项目配置

脚本中固定的项目配置类似:

$ProjectId = "8702461"
$OpenApiFile = "E:\javaproject\plant-identifier\openapi.json"

Token 建议通过环境变量 APIFOX_ACCESS_TOKEN 提供。脚本如果读取不到 Token,可以在终端中安全输入。

五、日常同步流程

完整流程如下:

  1. 修改后端接口代码;
  2. 编译并验证后端项目;
  3. 重新生成 openapi.json
  4. 让 AI 校验 OpenAPI JSON;
  5. 必要时通过 MCP 查询 Apifox 中已有接口;
  6. 执行回写脚本;
  7. 查看同步结果;
  8. 确认 Apifox 文档和前端联调结果。

让 AI 执行脚本:

powershell -ExecutionPolicy Bypass `
  -File "E:\javaproject\apifox-import.ps1"

也可以直接对 AI 说:

请修改后端接口代码,重新生成 openapi.json,校验 JSON 格式,然后执行 E:\javaproject\apifox-import.ps1。
执行完成后汇报 endpointCreated、endpointUpdated 和 endpointFailed。

六、AUTO_MERGE 是什么

脚本使用:

endpointOverwriteBehavior = "AUTO_MERGE"

它按照“HTTP 方法 + 接口路径”匹配接口:

POST /api/auth/login  ≠  POST /api/auth/login1
GET  /api/user         ≠  POST /api/user

行为如下:

  • 本地有、Apifox 没有:创建接口;
  • 两边方法和路径相同:合并更新接口;
  • 方法或路径变化:会被视为新接口;
  • 不会自动删除路径变化前的旧接口。

AUTO_MERGE 适合日常同步,可以尽量保留 Apifox 中已有的 Mock、示例和手工说明。

七、目录同步

脚本中的:

updateFolderOfChangedEndpoint = $true

表示接口所属目录发生变化时,自动将接口移动到新目录。

它只负责目录同步,不负责:

  • 判断两个不同路径是不是同一个接口;
  • 删除旧接口;
  • 标记旧接口废弃。

八、旧接口如何处理

如果接口从代码和 openapi.json 中删除,但 Apifox 中仍然存在,脚本会将它识别为候选旧接口。

第一次建议只预览,不修改:

powershell -ExecutionPolicy Bypass `
  -File "E:\javaproject\apifox-import.ps1"

确认列出的接口确实应该废弃后,再执行:

powershell -ExecutionPolicy Bypass `
  -File "E:\javaproject\apifox-import.ps1" `
  -AutoDeprecateMissing

脚本会将这些接口标记为:

"x-apifox-status": "deprecated"

不会直接删除接口。推荐采用两阶段策略:

新接口上线
    ↓
旧接口标记 deprecated
    ↓
前端迁移并观察兼容期
    ↓
确认无依赖后人工删除旧接口

九、让 AI 执行完整协同流程

可以直接使用下面的指令:

请完成以下流程:

1. 修改后端接口代码;
2. 重新生成或更新 openapi.json;
3. 校验 OpenAPI JSON 格式;
4. 通过 Apifox MCP 查询相关接口;
5. 执行 E:\javaproject\apifox-import.ps1;
6. 先报告 Apifox 中存在、本地 OpenAPI 中不存在的接口;
7. 我确认后再追加 -AutoDeprecateMissing;
8. 最后汇报新增、更新、废弃和失败数量。

如果确认可以自动废弃:

请修改后端代码并更新 openapi.json,然后执行:
powershell -ExecutionPolicy Bypass -File "E:\javaproject\apifox-import.ps1" -AutoDeprecateMissing

不要删除接口,最后汇报新增、更新、废弃和失败数量。

十、同步结果怎么看

字段 含义
endpointCreated 新创建的接口数量
endpointUpdated 更新的已有接口数量
endpointFailed 失败的接口数量
schemaCreated 新创建的数据模型数量
schemaUpdated 更新的数据模型数量
schemaIgnored 已存在、无需更新的数据模型数量
废弃数量 Apifox 有、本地 OpenAPI 没有并被标记为 deprecated 的接口数量

正常情况下,重点关注:

endpointFailed = 0

如果 endpointCreated = 0endpointUpdated = 0,不一定是失败,也可能是两边接口已经完全一致。

十一、常见问题

AI 新建接口而不是更新

检查 HTTP 方法和路径是否完全一致。例如:

POST /api/auth/login
POST /api/auth/login1

这两个接口会被视为不同接口。

403012:No project maintainer privilege

Token 所属账号没有项目维护者权限。需要使用有权限的账号生成 Token,或调整项目成员权限。

404000:Not found

检查项目 ID 是否正确,以及当前 PowerShell 会话中的变量是否为空。

废弃数量正确,但客户端没有删除线

客户端列表不一定展示删除线。刷新 Apifox 在线文档,并查看接口状态是否为“将废弃”。

中文提示乱码

PowerShell 脚本可能因为文件编码造成中文提示乱码。乱码一般不影响接口请求;可以将脚本提示改成英文,或将脚本保存为 UTF-8 编码。

十二、安全与检查清单

  • [ ] 后端代码已编译通过;
  • [ ] openapi.json 已重新生成;
  • [ ] OpenAPI JSON 格式有效;
  • [ ] MCP 可以查询 Apifox 项目;
  • [ ] 脚本返回 endpointFailed = 0
  • [ ] 新增和更新数量符合预期;
  • [ ] 疑似旧接口清单已经人工确认;
  • [ ] 废弃接口没有被直接删除;
  • [ ] Token 没有提交到仓库、截图或文章中。

总结

这套方案可以概括为:

> OpenAPI 是代码侧接口定义,Apifox 是团队协作与交付平台,MCP 负责读取,脚本负责回写,AI 负责把这些动作串起来。

以后每次接口开发完成,只需要确保 openapi.json 更新,然后让 AI 执行同步脚本即可。

附录:

apifox-import.ps1

param(
    [string]$Token = $env:APIFOX_ACCESS_TOKEN,

    [switch]$AutoDeprecateMissing
)

$ErrorActionPreference = "Stop"

$ProjectId = "8702461"
$OpenApiFile = "E:\javaproject\plant-identifier\openapi.json"
$ApiVersion = "2024-03-28"
$BaseUrl = "https://api.apifox.com/v1/projects/$ProjectId"

$HttpMethods = @(
    "get",
    "post",
    "put",
    "delete",
    "patch",
    "head",
    "options",
    "trace"
)

if ([string]::IsNullOrWhiteSpace($Token)) {
    $secureToken = Read-Host "Enter Apifox token" -AsSecureString
    $Token = [System.Net.NetworkCredential]::new("", $secureToken).Password
}

if ([string]::IsNullOrWhiteSpace($Token)) {
    throw "No Apifox token was provided."
}

if (!(Test-Path -LiteralPath $OpenApiFile -PathType Leaf)) {
    throw "OpenAPI file not found: $OpenApiFile"
}

$Headers = @{
    Authorization = "Bearer $Token"
    "X-Apifox-Api-Version" = $ApiVersion
}

function Get-EndpointKey {
    param(
        [string]$Path,
        [string]$Method
    )

    return "$($Method.ToUpperInvariant()) $Path"
}

function Get-EndpointKeys {
    param(
        $Document
    )

    $keys = @{}

    if ($null -eq $Document.paths) {
        return $keys
    }

    foreach ($pathProperty in $Document.paths.PSObject.Properties) {
        $path = $pathProperty.Name
        $pathItem = $pathProperty.Value

        foreach ($method in $HttpMethods) {
            $operationProperty = $pathItem.PSObject.Properties[$method]

            if ($null -ne $operationProperty -and $null -ne $operationProperty.Value) {
                $key = Get-EndpointKey -Path $path -Method $method
                $keys[$key] = $true
            }
        }
    }

    return $keys
}

function Import-OpenApi {
    param(
        $Document,

        [ValidateSet(
            "AUTO_MERGE",
            "OVERWRITE_EXISTING",
            "KEEP_EXISTING",
            "CREATE_NEW"
        )]
        [string]$OverwriteBehavior = "AUTO_MERGE"
    )

    $specText = $Document | ConvertTo-Json -Depth 100 -Compress

    $payloadObject = @{
        input = $specText
        options = @{
            endpointOverwriteBehavior = $OverwriteBehavior
            updateFolderOfChangedEndpoint = $true
        }
    }

    $payload = $payloadObject | ConvertTo-Json -Depth 100 -Compress

    return Invoke-RestMethod `
        -Method Post `
        -Uri "$BaseUrl/import-openapi?locale=zh-CN" `
        -Headers $Headers `
        -ContentType "application/json; charset=utf-8" `
        -Body $payload `
        -TimeoutSec 120
}

function Export-ApifoxOpenApi {
    $exportPayloadObject = @{
        scope = @{
            type = "ALL"
        }
        options = @{
            includeApifoxExtensionProperties = $true
            addFoldersToTags = $false
        }
        oasVersion = "3.0"
        exportFormat = "JSON"
    }

    $exportPayload = $exportPayloadObject | ConvertTo-Json -Depth 20 -Compress

    return Invoke-RestMethod `
        -Method Post `
        -Uri "$BaseUrl/export-openapi?locale=zh-CN" `
        -Headers $Headers `
        -ContentType "application/json; charset=utf-8" `
        -Body $exportPayload `
        -TimeoutSec 120
}

Write-Host "Reading local OpenAPI..."

$localSpecText = [System.IO.File]::ReadAllText($OpenApiFile)
$localSpec = $localSpecText | ConvertFrom-Json

Write-Host "Importing local OpenAPI with AUTO_MERGE..."

$importResult = Import-OpenApi `
    -Document $localSpec `
    -OverwriteBehavior "AUTO_MERGE"

Write-Host ""
Write-Host "Local OpenAPI import result:"
$importResult | ConvertTo-Json -Depth 20

Write-Host ""
Write-Host "Exporting current Apifox OpenAPI..."

$apifoxSpec = Export-ApifoxOpenApi

$localKeys = Get-EndpointKeys -Document $localSpec
$missingEndpoints = @()

if ($null -ne $apifoxSpec.paths) {
    foreach ($pathProperty in $apifoxSpec.paths.PSObject.Properties) {
        $path = $pathProperty.Name
        $pathItem = $pathProperty.Value

        foreach ($method in $HttpMethods) {
            $operationProperty = $pathItem.PSObject.Properties[$method]

            if ($null -eq $operationProperty -or $null -eq $operationProperty.Value) {
                continue
            }

            $key = Get-EndpointKey -Path $path -Method $method

            if (!$localKeys.ContainsKey($key)) {
                $missingEndpoints += [PSCustomObject]@{
                    Method = $method.ToUpperInvariant()
                    Path = $path
                    Key = $key
                }
            }
        }
    }
}

Write-Host ""
Write-Host "Endpoints existing in Apifox but missing from local OpenAPI: $($missingEndpoints.Count)"

if ($missingEndpoints.Count -eq 0) {
    Write-Host "No deprecated candidates found."
    Write-Host "Completed."
    exit 0
}

$missingEndpoints | Format-Table Method, Path -AutoSize

if (!$AutoDeprecateMissing) {
    Write-Host ""
    Write-Host "Preview only. No endpoint status was changed."
    Write-Host "To mark these endpoints as deprecated, run:"
    Write-Host "powershell -ExecutionPolicy Bypass -File `"$PSCommandPath`" -AutoDeprecateMissing"
    exit 0
}

Write-Host ""
Write-Host "Preparing deprecated endpoint update..."

$deprecatedPaths = [ordered]@{}

foreach ($item in $missingEndpoints) {
    $pathProperty = $apifoxSpec.paths.PSObject.Properties[$item.Path]

    if ($null -eq $pathProperty) {
        continue
    }

    $pathItem = $pathProperty.Value
    $methodName = $item.Method.ToLowerInvariant()
    $operationProperty = $pathItem.PSObject.Properties[$methodName]

    if ($null -eq $operationProperty) {
        continue
    }

    $operation = $operationProperty.Value

    $statusProperty = $operation.PSObject.Properties["x-apifox-status"]

    if ($null -eq $statusProperty) {
        $operation | Add-Member `
            -NotePropertyName "x-apifox-status" `
            -NotePropertyValue "deprecated" `
            -Force
    }
    else {
        $statusProperty.Value = "deprecated"
    }

    $marker = "[Deprecated: missing from local OpenAPI]"

    $descriptionProperty = $operation.PSObject.Properties["description"]

    if ($null -eq $descriptionProperty) {
        $operation | Add-Member `
            -NotePropertyName "description" `
            -NotePropertyValue $marker `
            -Force
    }
    elseif ([string]$descriptionProperty.Value -notlike "*Deprecated: missing from local OpenAPI*") {
        $descriptionProperty.Value = "$($descriptionProperty.Value)`n`n$marker"
    }

    if (!$deprecatedPaths.Contains($item.Path)) {
        $deprecatedPaths[$item.Path] = [ordered]@{}
    }

    $deprecatedPaths[$item.Path][$methodName] = $operation
}

$deprecatedSpec = [ordered]@{
    openapi = "3.0.0"
    info = [ordered]@{
        title = "Deprecated endpoint updates"
        version = "1.0.0"
    }
    paths = $deprecatedPaths
}

if ($null -ne $apifoxSpec.components) {
    $deprecatedSpec.components = $apifoxSpec.components
}

if ($null -ne $apifoxSpec.servers) {
    $deprecatedSpec.servers = $apifoxSpec.servers
}

Write-Host "Updating deprecated endpoints with OVERWRITE_EXISTING..."

$deprecatedResult = Import-OpenApi `
    -Document $deprecatedSpec `
    -OverwriteBehavior "OVERWRITE_EXISTING"

Write-Host ""
Write-Host "Deprecated endpoint update result:"
$deprecatedResult | ConvertTo-Json -Depth 20

Write-Host ""
Write-Host "Deprecated endpoints:"
$missingEndpoints | Format-Table Method, Path -AutoSize

Write-Host ""
Write-Host "Completed."

Logo

AtomGit AI 社区提供模型库、数据集、Agent、Token等资源

更多推荐