Engee 文档

用于访问Engee的外部编程接口

*Engee*为第三方应用程序的开发人员提供编程接口(HTTP API),允许第三方应用程序登录*Engee*并在*Engee*系统中代表用户执行操作。

授权书

通过标准OAuth机制对*Engee*中的第三方应用程序的用户进行授权。

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

  • 'redirect_uri’是用户将被重定向到的第三方应用程序中的URL。

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

  • 'client_id’是第三方应用程序的ID。;

  • 'client_secret’是第三方应用程序的密钥。;

  • 'scope'-此应用程序的授权API。


它是如何工作的?

  1. 第三方应用程序将用户引导到"授权URL"(https://engee.com/account/authorize)具有以下查询参数:

  2. 在用户同意提供数据后,他将被重定向回`redirect_uri`上的第三方应用程序,具有以下查询参数:

  3. 之后,您需要获得令牌。 为此,向"令牌URL"发送"POST"请求(https://engee.com/account/api/oauth2/token),具有以下正文('application/x-www-form-urlencoded'):

    • 'grant_type:"authorization_code"—-固定值;

    • '代码—-在第二步中收到的代码。;

    • 'client_id’是第三方应用程序的ID。;

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

    • "范围`-此应用程序的授权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’是第三方应用程序的密钥。;

    • "范围`-此应用程序的授权API;

    • 'redirect_uri’是用户将被重定向到的第三方应用程序中的URL。

      访问令牌寿命:10分钟,刷新令牌:30天.
  4. 接下来,能够使用接收到的令牌获取用户数据。 要做到这一点,发送一个带有标题的"GET"请求到 https://engee.com/account/api/oauth2/session.

    请求示例:

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

    响应示例:

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

与工程师的互动

为了使所有Api与*Engee*交互,系统必须由用户启动(在个人帐户中按下启动*Engee*按钮)。 如果无法按下开始按钮,那么您还可以自动执行*Engee*的启动和停止(请参阅下面对*Engee*控制方法的描述)。

必须将authorization标头"Authorization:Bearer$token"添加到所有请求中(该令牌是在授权阶段收到的)。

API方法

所有管理Api的前缀*Engee* — 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
}

"删除/帐户/api/engee/停止—-停止*Engee*。 返回响应代码204。

所有其他Api的前缀具有以下形式:`https://engee.com/prod/user每个用户都有自己的前缀,必须从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"
        }
      }

      这里:

      • '错误。full_description’是错误的完整描述以及堆栈跟踪。 输出类似于错误输出到终端;

      • '错误。short_description—-没有堆栈跟踪的异常;

      • '错误。类型`-错误类型。

  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。;

  • 如果该文件已经存在,则会在其名称中添加后缀。;

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

下载文件

'发布/外部/文件/下载`

请求:

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

  • 如果有几个文件,它们被压缩到ZIP中,并且已经返回`application/x-zip-compressed'。;

  • 如果至少一个文件不存在,将返回404作为响应。;

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

异步代码执行

要执行执行时间可能超过HTTP连接超时的代码,请使用异步API。 这种方法避免了在执行长期操作时断开连接。

同步和异步执行之间的选择:

  • 当需要立即响应时,使用`POST/external/command/eval’快速执行短命令。

  • 使用异步API(在创建后台任务部分中描述)进行长期计算,您可以稍后检查结果。

创建后台任务

`POST/external/command/jobs/create'

在后台启动代码执行。 服务器接受任务并立即返回响应,而无需等待代码执行完成。

请求主体(application/json):

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "job_type": "EVAL",
  "code": "a = 5; sleep(60); a * 2",
  "name": "我的计算"
}

参数:

  • 'job_id’是唯一的任务标识符(UUID)。

  • 'job_type'--任务类型(`"EVAL"')。

  • 'code'--要执行的代码。

  • "名称"是一个可选的任务名称,以便于识别。

答案:

  • '202Accepted'--任务已成功创建并排队等待执行。

  • '409Conflict'--指定’job_id’的任务已经存在。

    如果您发送具有现有`job_id`的请求,系统将返回状态`409Conflict`,并且不会创建重复的任务。

检查进度状态

'获取/外部/命令/作业/{job_id}/结果`

允许您获取任务的当前状态及其结果(当任务完成时)。

回答:

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "我的计算",
  "job_type": "EVAL",
  "status": "IN_PROGRESS",
  "start_time": "2024-05-20T12:00:00Z",
  "end_time": null,
  "result": null
}

响应字段:

  • 'status'--执行状态``"IN_PROGRESS"`(进行中)或`"DONE"'(完成)。

  • 'start_time'--任务的开始时间。

  • 'end_time'--任务完成时间(在status=`"DONE"'时填写)。

  • 'result'--包含执行结果或错误(在`status="DONE"'时填写)。

成功完成后的结果格式:

{
  "type": "success",
  "data": {
    "text": "10"
  }
}

执行错误时的结果格式:

{
  "type": "error",
  "data": {
    "type": "UndefVarError",
    "short_description": "UndefVarError(:some_variable)",
    "full_description": "UndefVarError: `some_variable` not defined\n\nStacktrace:\n..."
  }
}

答案:

  • '200OK'--任务信息已收到。

  • `404Not Found'--未找到具有指定`job_id`的任务。

获取问题列表

'获取/外部/命令/作业/列表`

检索用户创建的所有任务的列表。

回答:

{
  "jobs": [
    {
      "job_id": "16763be4-6022-406e-a950-fcd5018633ca",
      "name": "task_1",
      "job_type": "EVAL",
      "status": "DONE",
      "start_time": "2024-05-20T12:00:00Z",
      "end_time": "2024-05-20T12:05:00Z",
      "result": {
        "type": "success",
        "data": {
          "text": "执行结果..."
        }
      }
    }
  ]
}

响应包含一个具有单个字段"jobs"的对象-任务数组。 将为每个任务返回完整信息,包括:

  • 唯一标识符('job_id`)。

  • 执行状态’status')。

  • 开始和结束时间戳。

  • 执行的结果(如果任务完成)。