Engee 文档

用于访问 Engee 的外部软件接口

*Engee*为第三方应用程序开发人员提供了一个编程接口(HTTP API),允许第三方应用程序向*Engee*授权,并代表*Engee*系统上的用户执行操作。

授权

第三方应用程序用户在*Engee*的授权是通过标准的OAuth机制完成的。

第三方应用程序开发人员向 Engee 开发人员传递以下信息:

  • redirect_uri - 用户将被返回到第三方应用程序中的 URL。

Engee*开发人员将以下信息传递给第三方应用程序开发人员:

  • client_id - 第三方应用程序的 ID;

  • client_secret - 第三方应用程序的密钥;

  • 范围"--该应用程序的授权 API。


它是如何工作的?

  1. 第三方应用程序使用以下查询参数引导用户访问 Authorise URL (https://engee.com/account/authorize):

  2. 用户同意提供数据后,他将被重定向到位于 redirect_uri 的第三方应用程序,查询参数如下:

  3. 然后,需要获取令牌。方法是向 "令牌 URL"(https://engee.com/account/api/oauth2/token )发送一个 "POST "请求,请求内容如下("application/x-www-form-urlencoded"):

    • grant_type: "authorisation_code" - 固定值;

    • code - 在第二步中获得的代码;

    • client_id - 第三方应用程序的 ID;

    • client_secret - 第三方应用程序的密钥;

    • scope - 该应用程序的授权 API;

    • redirect_uri - 用户将被返回到第三方应用程序中的 URL;

      请求示例:

      curl -d grant_type=authorization_code -d code=$code -d client_id=$client_id -d client_secret=$client_secret -d "scope=user:id user:username profile:name profile:primary_email engee" -d redirect_uri=$redirect_uri https://engee.com/account/api/oauth2/token

      作为回应,将发送一个访问令牌,用于授权对 HTTP API 的所有后续请求。同时还会发送刷新令牌,这是更新访问令牌所必需的。

    要刷新令牌,必须向地址 `POST`https://engee.com/account/api/oauth2/token 发送带参数的请求:

    • grant_type: "refresh_token" - 固定值;

    • refresh_token - 刷新令牌;

    • client_id - 第三方应用程序的 ID;

    • client_secret - 第三方应用程序的密钥;

    • scope - 该应用程序的授权 API;

    • redirect_uri - 用户将被返回到第三方应用程序中的 URL。

      访问令牌有效期:10 分钟,刷新令牌有效期:30 天。
  4. 此外,还可以使用接收到的令牌获取用户数据,为此,有必要向https://engee.com/account/api/oauth2/session 发送一个带有标头("Header")的 "GET "请求。

    请求示例

    curl -H "Authorization: Bearer $token" https://engee.com/account/api/oauth2/session

    响应示例

    {"sub":"1d592c69-9664-4eb5-9166-447c421a02df","nickname":"username","email":"username@email.tld"}

与恩吉的互动

所有与*Engee*互动的应用程序接口都必须由用户启动(按下个人账户中的启动*Engee*按钮),系统才能运行。如果无法按下启动按钮,也可以自动启动和停止*Engee*(见下文*Engee*管理方法说明)。

所有请求都应添加授权标题 "Authorisation: Bearer $token"(在授权阶段收到的令牌)。

API 方法

所有 Engee Authority API 的前缀 -https://engee.com.

恩吉管理

GET /account/api/engee/info - 获取 Engee 状态信息。响应体:

{
"username": "string",
"serverName": "string",
"serverId": "string",
"url": "string",
"serverStatus": "starting"|"running"|"stopping"|"stopped",
"startingAt": "2025-02-10T10:24:31.328Z",
"startedAt": "2025-02-10T10:24:31.328Z",
"stoppingAt": "2025-02-10T10:24:31.328Z",
"stoppedAt": "2025-02-10T10:24:31.328Z",
"lastActivityAt": "2025-02-10T10:24:31.328Z",
"inactivityTimeout": 0,
"stopSessionAt": "2025-02-10T10:24:31.328Z",
"clusterNamespace": {
"minimalMode": true
}

POST /account/api/engee/start` - 启动*Engee*(类似于点击个人账户中的 "启动*Engee*"按钮)。响应体(要启动的服务器的 URL):

{
'server': string
}

DELETE /account/api/engee/stop - 停止 Engee。返回响应代码 204。

所有其他 API 的前缀形式为https://engee.com/prod/user/$license-$nickname/`。每个用户的前缀都不同,应从 URL 字段的 Engee 状态响应中获取。

执行任意代码

POST /external/command/eval`.

  1. 最简单的查询查询:

    {
      "command": "3 + 5"
    }

    返回数据

    • 状态代码:200。

      {
        "result": "8"
      }
  2. 源代码执行过程中出错。请求:

    {
      "command": "unexisting_variable"
    }

    返回数据

    • 状态代码:400。

      {
        "result_code": "command_error",
        "error": {
          "full_description": "UndefVarError: `unexisting_variable` not defined\n\nStacktrace:\n  [1] top-level scope\n    @ none:1\n  [2] eval(m::Module, e::Any)\n    @ Core ./boot.jl:370\n  [3] top-level scope\n    @ none:4\n  [4] eval\n    @ ./boot.jl:370 [inlined]\n  [5] eval_mask_code(code::String)\n    @ Main.Masks /app/IJulia/preload_cells/masks.jl:206\n  [6] eval_mask_codes(mask_codes::Vector{Main.Masks.MaskCode})\n    @ Main.Masks /app/IJulia/preload_cells/masks.jl:197\n  [7] |>\n    @ ./operators.jl:907 [inlined]\n  [8] evaluate_mask_codes(mask_codes::Vector{Dict{String, String}})\n    @ Main.Masks /app/IJulia/preload_cells/masks.jl:272\n  [9] |>\n    @ ./operators.jl:907 [inlined]\n [10] (::Workspaces.Servers.var\"#eval_mask_codes#157\"{typeof(Main.Masks.evaluate_mask_codes), Workspaces.Servers.var\"#convert_mask_result#156\"})(blocks::Vector{Dict{String, String}})\n    @ Workspaces.Servers /usr/local/julia-1.9.3/packages/Workspaces/2XYbD/src/servers/handlers.jl:539\n [11] request_pipeline(req::HTTP.Messages.Request, log_func::var\"#13#14\"{String}, req_params_func::Function, log_response_func::Workspaces.Servers.var\"#mask_log_and_response#159\"{Workspaces.Servers.var\"#result_to_response#158\"}, pipeline_stages::Workspaces.Servers.PipelineStages)\n    @ Workspaces.Servers /usr/local/julia-1.9.3/packages/Workspaces/2XYbD/src/servers/handlers_common.jl:282\n [12] required_body_request_pipeline\n    @ /usr/local/julia-1.9.3/packages/Workspaces/2XYbD/src/servers/handlers_common.jl:289 [inlined]\n [13] eval_mask_code_handler\n    @ /usr/local/julia-1.9.3/packages/Workspaces/2XYbD/src/servers/handlers.jl:556 [inlined]\n [14] (::Workspaces.Servers.var\"#eval_mask_code#186\"{var\"#13#14\"{String}, typeof(Main.Masks.evaluate_mask_codes)})(req::HTTP.Messages.Request)\n    @ Workspaces.Servers /usr/local/julia-1.9.3/packages/Workspaces/2XYbD/src/servers/Servers.jl:68\n [15] (::HTTP.Handlers.Router{typeof(HTTP.Handlers.default404), typeof(HTTP.Handlers.default405), Nothing})(req::HTTP.Messages.Request)\n    @ HTTP.Handlers /usr/local/julia-1.9.3/packages/HTTP/sJD5V/src/Handlers.jl:439\n [16] (::HTTP.Handlers.var\"#1#2\"{HTTP.Handlers.Router{typeof(HTTP.Handlers.default404), typeof(HTTP.Handlers.default405), Nothing}})(stream::HTTP.Streams.Stream{HTTP.Messages.Request, HTTP.Connections.Connection{Sockets.TCPSocket}})\n    @ HTTP.Handlers /usr/local/julia-1.9.3/packages/HTTP/sJD5V/src/Handlers.jl:58\n [17] #invokelatest#2\n    @ ./essentials.jl:819 [inlined]\n [18] invokelatest\n    @ ./essentials.jl:816 [inlined]\n [19] handle_connection(f::Function, c::HTTP.Connections.Connection{Sockets.TCPSocket}, listener::HTTP.Servers.Listener{Nothing, Sockets.TCPServer}, readtimeout::Int64, access_log::Nothing)\n    @ HTTP.Servers /usr/local/julia-1.9.3/packages/HTTP/sJD5V/src/Servers.jl:469\n [20] macro expansion\n    @ /usr/local/julia-1.9.3/packages/HTTP/sJD5V/src/Servers.jl:401 [inlined]\n [21] (::HTTP.Servers.var\"#16#17\"{HTTP.Handlers.var\"#1#2\"{HTTP.Handlers.Router{typeof(HTTP.Handlers.default404), typeof(HTTP.Handlers.default405), Nothing}}, HTTP.Servers.Listener{Nothing, Sockets.TCPServer}, Set{HTTP.Connections.Connection}, Int64, Nothing, ReentrantLock, Base.Semaphore, HTTP.Connections.Connection{Sockets.TCPSocket}})()\n    @ HTTP.Servers ./task.jl:514",
          "short_description": "UndefVarError(:unexisting_variable)",
          "type": "UndefVarError"
        }
      }

      这里:

      • error.full_description - 错误的完整描述以及堆栈跟踪。输出类似于错误在终端中的输出;

      • error.short_description - 没有堆栈跟踪的异常;

      • error.type - 错误类型。

  3. 返回 JSON 格式。查询:

    {
      "command": "using JSON\nJSON.json(\"a\"=>1)"
    }

    返回数据

    • 状态代码:200。

      {
        "result": "{\"a\":1}"
      }

上传文件

POST /external/file/upload.

请求以 multipart/form-data 格式发送。

路径 "字段包含以下格式的 JSON:

{
  "sources": [
    "/user/some_dir"
  ]
}

此处:

  • sources 内的每个元素都表示要上传文件的目录;

  • upload_files 字段包含要上传的文件;

  • upload_files 字段的数量必须与传递的目录数量一致。

请求示例

curl -X 'POST' \
  'https://engee.com/prod/user/$license-$user/external/file/upload' \
  -H 'Authorization: Bearer $access_token' \
  -H 'Content-Type: multipart/form-data' \
  -F 'paths={
  "sources": [
    "/user/some_directory"
  ]
}' \
  -F 'upload_files=@2.png;type=image/png'
  • 如果下载文件的目录不存在,则将创建该目录;

  • 所有下载的文件必须位于 `/user`目录内,否则将返回错误 403;

  • 如果文件已经存在,将在其名称后添加一个后缀;

  • 只有在下载完成后,文件才会被写入光盘。如果下载中断,文件将不会出现在文件系统中。

下载文件

POST /external/file/download`.

请求:

{
  "sources": [
    "/user/some_file"
  ]
}
  • 如果是单个文件,则以 multipart/form-data 格式返回;

  • 如果有多个文件,则对它们进行 ZIP 压缩,并已返回 application/x-zip-compressed 格式;

  • 如果至少有一个文件不存在,将返回 404;

  • 所有下载的文件必须位于 /user 目录内,否则将返回错误 403。