2021-11-09 03:45:30 +08:00
|
|
|
defmodule LivebookWeb.Output do
|
2023-02-23 02:34:54 +08:00
|
|
|
use LivebookWeb, :html
|
2021-11-09 03:45:30 +08:00
|
|
|
|
2022-01-17 20:24:59 +08:00
|
|
|
alias LivebookWeb.Output
|
|
|
|
|
2021-11-09 03:45:30 +08:00
|
|
|
@doc """
|
2023-10-28 02:49:46 +08:00
|
|
|
Renders a single cell output.
|
2021-11-09 03:45:30 +08:00
|
|
|
"""
|
2023-10-28 02:49:46 +08:00
|
|
|
attr :id, :string, required: true
|
|
|
|
attr :output, :map, required: true
|
2023-05-26 19:12:41 +08:00
|
|
|
attr :session_id, :string, required: true
|
|
|
|
attr :session_pid, :any, required: true
|
2023-07-13 18:26:10 +08:00
|
|
|
attr :input_views, :map, required: true
|
2023-05-26 19:12:41 +08:00
|
|
|
attr :client_id, :string, required: true
|
|
|
|
attr :cell_id, :string, required: true
|
|
|
|
|
2023-10-28 02:49:46 +08:00
|
|
|
def output(assigns) do
|
2021-11-26 01:43:42 +08:00
|
|
|
~H"""
|
2023-10-28 02:49:46 +08:00
|
|
|
<div id={@id} class="max-w-full" data-el-output data-border={border?(@output)}>
|
|
|
|
<%= render_output(@output, %{
|
|
|
|
id: "#{@id}-output",
|
2023-02-23 02:34:54 +08:00
|
|
|
session_id: @session_id,
|
|
|
|
session_pid: @session_pid,
|
2023-07-13 18:26:10 +08:00
|
|
|
input_views: @input_views,
|
2023-05-26 19:12:41 +08:00
|
|
|
client_id: @client_id,
|
|
|
|
cell_id: @cell_id
|
2023-02-23 02:34:54 +08:00
|
|
|
}) %>
|
|
|
|
</div>
|
2021-11-26 01:43:42 +08:00
|
|
|
"""
|
|
|
|
end
|
|
|
|
|
2023-09-26 10:39:20 +08:00
|
|
|
defp border?(%{type: :terminal_text}), do: true
|
2023-08-24 04:39:36 +08:00
|
|
|
defp border?(%{type: :error, context: {:interrupt, _, _}}), do: false
|
2023-08-24 05:25:04 +08:00
|
|
|
defp border?(%{type: :error}), do: true
|
|
|
|
defp border?(%{type: :grid, boxed: boxed}), do: boxed
|
2022-01-17 03:37:00 +08:00
|
|
|
defp border?(_output), do: false
|
2021-11-26 01:43:42 +08:00
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(%{type: :terminal_text, text: text}, %{id: id}) do
|
2022-01-17 03:37:00 +08:00
|
|
|
text = if(text == :__pruned__, do: nil, else: text)
|
2023-10-05 21:27:34 +08:00
|
|
|
assigns = %{id: id, text: text}
|
|
|
|
|
|
|
|
~H"""
|
|
|
|
<.live_component module={Output.TerminalTextComponent} id={@id} text={@text} />
|
|
|
|
"""
|
2021-11-09 03:45:30 +08:00
|
|
|
end
|
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(%{type: :plain_text, text: text}, %{id: id}) do
|
2023-08-22 19:21:22 +08:00
|
|
|
text = if(text == :__pruned__, do: nil, else: text)
|
2023-10-05 21:27:34 +08:00
|
|
|
assigns = %{id: id, text: text}
|
|
|
|
|
|
|
|
~H"""
|
|
|
|
<.live_component module={Output.PlainTextComponent} id={@id} text={@text} />
|
|
|
|
"""
|
2023-03-16 00:48:38 +08:00
|
|
|
end
|
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(%{type: :markdown, text: text}, %{id: id, session_id: session_id}) do
|
2023-08-22 19:21:22 +08:00
|
|
|
text = if(text == :__pruned__, do: nil, else: text)
|
2023-10-05 21:27:34 +08:00
|
|
|
assigns = %{id: id, session_id: session_id, text: text}
|
|
|
|
|
|
|
|
~H"""
|
|
|
|
<.live_component module={Output.MarkdownComponent} id={@id} session_id={@session_id} text={@text} />
|
|
|
|
"""
|
2021-11-09 03:45:30 +08:00
|
|
|
end
|
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(%{type: :image} = output, %{id: id}) do
|
|
|
|
assigns = %{id: id, content: output.content, mime_type: output.mime_type}
|
2022-01-17 03:37:00 +08:00
|
|
|
|
|
|
|
~H"""
|
2022-12-02 19:42:31 +08:00
|
|
|
<Output.ImageComponent.render id={@id} content={@content} mime_type={@mime_type} />
|
2022-01-17 03:37:00 +08:00
|
|
|
"""
|
2021-11-09 03:45:30 +08:00
|
|
|
end
|
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(%{type: :js} = output, %{
|
|
|
|
id: id,
|
|
|
|
session_id: session_id,
|
|
|
|
client_id: client_id
|
|
|
|
}) do
|
2023-10-05 21:27:34 +08:00
|
|
|
assigns = %{
|
2022-02-28 20:53:33 +08:00
|
|
|
id: id,
|
2023-08-24 05:25:04 +08:00
|
|
|
js_view: output.js_view,
|
2022-03-23 01:25:42 +08:00
|
|
|
session_id: session_id,
|
2023-10-05 21:27:34 +08:00
|
|
|
client_id: client_id
|
|
|
|
}
|
|
|
|
|
|
|
|
~H"""
|
|
|
|
<.live_component
|
|
|
|
module={LivebookWeb.JSViewComponent}
|
|
|
|
id={@id}
|
|
|
|
js_view={@js_view}
|
|
|
|
session_id={@session_id}
|
|
|
|
client_id={@client_id}
|
|
|
|
timeout_message="Output data no longer available, please reevaluate this cell"
|
|
|
|
/>
|
|
|
|
"""
|
2021-11-26 01:43:42 +08:00
|
|
|
end
|
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(%{type: :frame} = output, %{
|
2022-01-17 03:37:00 +08:00
|
|
|
id: id,
|
2022-08-05 20:43:41 +08:00
|
|
|
session_id: session_id,
|
2023-02-02 01:49:12 +08:00
|
|
|
session_pid: session_pid,
|
2023-07-13 18:26:10 +08:00
|
|
|
input_views: input_views,
|
2023-05-26 19:12:41 +08:00
|
|
|
client_id: client_id,
|
|
|
|
cell_id: cell_id
|
2022-01-17 03:37:00 +08:00
|
|
|
}) do
|
2023-10-05 21:27:34 +08:00
|
|
|
assigns = %{
|
2022-01-17 03:37:00 +08:00
|
|
|
id: id,
|
2023-08-24 05:25:04 +08:00
|
|
|
outputs: output.outputs,
|
|
|
|
placeholder: output.placeholder,
|
2022-01-17 03:37:00 +08:00
|
|
|
session_id: session_id,
|
2023-02-02 01:49:12 +08:00
|
|
|
session_pid: session_pid,
|
2023-07-13 18:26:10 +08:00
|
|
|
input_views: input_views,
|
2023-05-26 19:12:41 +08:00
|
|
|
client_id: client_id,
|
|
|
|
cell_id: cell_id
|
2023-10-05 21:27:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
~H"""
|
|
|
|
<.live_component
|
|
|
|
module={Output.FrameComponent}
|
|
|
|
id={@id}
|
|
|
|
outputs={@outputs}
|
|
|
|
placeholder={@placeholder}
|
|
|
|
session_id={@session_id}
|
|
|
|
session_pid={@session_pid}
|
|
|
|
input_views={@input_views}
|
|
|
|
client_id={@client_id}
|
|
|
|
cell_id={@cell_id}
|
|
|
|
/>
|
|
|
|
"""
|
2022-01-17 03:37:00 +08:00
|
|
|
end
|
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(%{type: :tabs, outputs: outputs, labels: labels}, %{
|
2022-08-04 06:24:15 +08:00
|
|
|
id: id,
|
|
|
|
session_id: session_id,
|
2023-02-06 19:00:13 +08:00
|
|
|
session_pid: session_pid,
|
2023-07-13 18:26:10 +08:00
|
|
|
input_views: input_views,
|
2023-05-26 19:12:41 +08:00
|
|
|
client_id: client_id,
|
|
|
|
cell_id: cell_id
|
2022-08-04 06:24:15 +08:00
|
|
|
}) do
|
|
|
|
assigns = %{
|
|
|
|
id: id,
|
|
|
|
labels: labels,
|
|
|
|
outputs: outputs,
|
|
|
|
session_id: session_id,
|
2023-02-06 19:00:13 +08:00
|
|
|
session_pid: session_pid,
|
2023-07-13 18:26:10 +08:00
|
|
|
input_views: input_views,
|
2023-05-26 19:12:41 +08:00
|
|
|
client_id: client_id,
|
|
|
|
cell_id: cell_id
|
2022-08-04 06:24:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
~H"""
|
2023-10-28 02:49:46 +08:00
|
|
|
<.live_component
|
|
|
|
module={Output.TabsComponent}
|
|
|
|
id={@id}
|
|
|
|
outputs={@outputs}
|
|
|
|
labels={@labels}
|
|
|
|
session_id={@session_id}
|
|
|
|
session_pid={@session_pid}
|
|
|
|
input_views={@input_views}
|
|
|
|
client_id={@client_id}
|
|
|
|
cell_id={@cell_id}
|
|
|
|
/>
|
2022-08-04 06:24:15 +08:00
|
|
|
"""
|
|
|
|
end
|
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(%{type: :grid} = grid, %{
|
2022-08-04 06:24:15 +08:00
|
|
|
id: id,
|
|
|
|
session_id: session_id,
|
2023-02-06 19:00:13 +08:00
|
|
|
session_pid: session_pid,
|
2023-07-13 18:26:10 +08:00
|
|
|
input_views: input_views,
|
2023-05-26 19:12:41 +08:00
|
|
|
client_id: client_id,
|
|
|
|
cell_id: cell_id
|
2022-08-04 06:24:15 +08:00
|
|
|
}) do
|
|
|
|
assigns = %{
|
|
|
|
id: id,
|
2023-08-24 05:25:04 +08:00
|
|
|
columns: grid.columns,
|
|
|
|
gap: grid.gap,
|
|
|
|
outputs: grid.outputs,
|
2022-08-04 06:24:15 +08:00
|
|
|
session_id: session_id,
|
2023-02-06 19:00:13 +08:00
|
|
|
session_pid: session_pid,
|
2023-07-13 18:26:10 +08:00
|
|
|
input_views: input_views,
|
2023-05-26 19:12:41 +08:00
|
|
|
client_id: client_id,
|
|
|
|
cell_id: cell_id
|
2022-08-04 06:24:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
~H"""
|
2023-10-28 02:49:46 +08:00
|
|
|
<.live_component
|
|
|
|
module={Output.GridComponent}
|
|
|
|
id={@id}
|
|
|
|
outputs={@outputs}
|
|
|
|
columns={@columns}
|
|
|
|
gap={@gap}
|
|
|
|
session_id={@session_id}
|
|
|
|
session_pid={@session_pid}
|
|
|
|
input_views={@input_views}
|
|
|
|
client_id={@client_id}
|
|
|
|
cell_id={@cell_id}
|
|
|
|
/>
|
2022-08-04 06:24:15 +08:00
|
|
|
"""
|
|
|
|
end
|
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(%{type: :input} = input, %{
|
2023-01-05 04:44:04 +08:00
|
|
|
id: id,
|
2023-07-13 18:26:10 +08:00
|
|
|
input_views: input_views,
|
2023-02-02 01:49:12 +08:00
|
|
|
session_pid: session_pid,
|
|
|
|
client_id: client_id
|
2023-01-05 04:44:04 +08:00
|
|
|
}) do
|
2023-10-05 21:27:34 +08:00
|
|
|
assigns = %{
|
2022-08-05 20:43:41 +08:00
|
|
|
id: id,
|
2023-08-24 05:25:04 +08:00
|
|
|
input: input,
|
2023-07-13 18:26:10 +08:00
|
|
|
input_views: input_views,
|
2023-02-02 01:49:12 +08:00
|
|
|
session_pid: session_pid,
|
|
|
|
client_id: client_id
|
2023-10-05 21:27:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
~H"""
|
|
|
|
<.live_component
|
|
|
|
module={Output.InputComponent}
|
|
|
|
id={@id}
|
|
|
|
input={@input}
|
|
|
|
input_views={@input_views}
|
|
|
|
session_pid={@session_pid}
|
|
|
|
client_id={@client_id}
|
|
|
|
/>
|
|
|
|
"""
|
2021-11-09 03:45:30 +08:00
|
|
|
end
|
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(%{type: :control} = control, %{
|
2022-08-05 20:43:41 +08:00
|
|
|
id: id,
|
2023-07-13 18:26:10 +08:00
|
|
|
input_views: input_views,
|
2023-02-02 01:49:12 +08:00
|
|
|
session_pid: session_pid,
|
2023-08-08 21:58:34 +08:00
|
|
|
client_id: client_id,
|
|
|
|
cell_id: cell_id
|
2022-08-05 20:43:41 +08:00
|
|
|
}) do
|
2023-10-05 21:27:34 +08:00
|
|
|
assigns = %{
|
2022-08-05 20:43:41 +08:00
|
|
|
id: id,
|
2023-08-24 05:25:04 +08:00
|
|
|
control: control,
|
2023-07-13 18:26:10 +08:00
|
|
|
input_views: input_views,
|
2023-02-02 01:49:12 +08:00
|
|
|
session_pid: session_pid,
|
2023-08-08 21:58:34 +08:00
|
|
|
client_id: client_id,
|
|
|
|
cell_id: cell_id
|
2023-10-05 21:27:34 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
~H"""
|
|
|
|
<.live_component
|
|
|
|
module={Output.ControlComponent}
|
|
|
|
id={@id}
|
|
|
|
control={@control}
|
|
|
|
input_views={@input_views}
|
|
|
|
session_pid={@session_pid}
|
|
|
|
client_id={@client_id}
|
|
|
|
cell_id={@cell_id}
|
|
|
|
/>
|
|
|
|
"""
|
2021-12-02 23:45:00 +08:00
|
|
|
end
|
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(
|
2023-08-24 04:39:36 +08:00
|
|
|
%{type: :error, context: {:missing_secret, secret_name}} = output,
|
2024-01-22 15:56:39 +08:00
|
|
|
%{session_id: session_id, id: id}
|
2023-08-24 05:25:04 +08:00
|
|
|
) do
|
2024-01-22 15:56:39 +08:00
|
|
|
assigns = %{message: output.message, secret_name: secret_name, session_id: session_id, id: id}
|
2021-11-09 03:45:30 +08:00
|
|
|
|
|
|
|
~H"""
|
2022-09-01 07:45:55 +08:00
|
|
|
<div class="-m-4 space-x-4 py-4">
|
|
|
|
<div
|
2022-09-06 05:59:13 +08:00
|
|
|
class="flex items-center justify-between border-b px-4 pb-4 mb-4"
|
2022-09-01 07:45:55 +08:00
|
|
|
style="color: var(--ansi-color-red);"
|
|
|
|
>
|
2022-09-06 05:59:13 +08:00
|
|
|
<div class="flex space-x-2 font-editor">
|
2022-09-01 07:45:55 +08:00
|
|
|
<.remix_icon icon="close-circle-line" />
|
2022-09-19 05:40:09 +08:00
|
|
|
<span>Missing secret <%= inspect(@secret_name) %></span>
|
2022-09-01 07:45:55 +08:00
|
|
|
</div>
|
2024-02-09 02:27:16 +08:00
|
|
|
<.button color="gray" patch={~p"/sessions/#{@session_id}/secrets?secret_name=#{@secret_name}"}>
|
2023-02-23 02:34:54 +08:00
|
|
|
Add secret
|
2024-02-09 02:27:16 +08:00
|
|
|
</.button>
|
2022-09-01 07:45:55 +08:00
|
|
|
</div>
|
2024-01-22 15:56:39 +08:00
|
|
|
<%= render_formatted_error_message(@id, @message) %>
|
2022-09-01 07:45:55 +08:00
|
|
|
</div>
|
2021-11-09 03:45:30 +08:00
|
|
|
"""
|
|
|
|
end
|
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(
|
2023-08-24 04:39:36 +08:00
|
|
|
%{type: :error, context: {:file_entry_forbidden, file_entry_name}} = output,
|
2024-01-22 15:56:39 +08:00
|
|
|
%{session_id: session_id, id: id}
|
2023-08-24 05:25:04 +08:00
|
|
|
) do
|
2024-01-22 15:56:39 +08:00
|
|
|
assigns = %{
|
|
|
|
message: output.message,
|
|
|
|
file_entry_name: file_entry_name,
|
|
|
|
session_id: session_id,
|
|
|
|
id: id
|
|
|
|
}
|
2023-07-18 08:00:11 +08:00
|
|
|
|
|
|
|
~H"""
|
|
|
|
<div class="-m-4 space-x-4 py-4">
|
|
|
|
<div
|
|
|
|
class="flex items-center justify-between border-b px-4 pb-4 mb-4"
|
|
|
|
style="color: var(--ansi-color-red);"
|
|
|
|
>
|
|
|
|
<div class="flex space-x-2 font-editor">
|
|
|
|
<.remix_icon icon="close-circle-line" />
|
|
|
|
<span>Forbidden access to file <%= inspect(@file_entry_name) %></span>
|
|
|
|
</div>
|
2024-02-09 02:27:16 +08:00
|
|
|
<.button
|
|
|
|
color="gray"
|
2023-07-18 08:00:11 +08:00
|
|
|
phx-click={JS.push("review_file_entry_access", value: %{name: @file_entry_name})}
|
|
|
|
>
|
|
|
|
Review access
|
2024-02-09 02:27:16 +08:00
|
|
|
</.button>
|
2023-07-18 08:00:11 +08:00
|
|
|
</div>
|
2024-01-22 15:56:39 +08:00
|
|
|
<%= render_formatted_error_message(@id, @message) %>
|
2023-07-18 08:00:11 +08:00
|
|
|
</div>
|
|
|
|
"""
|
|
|
|
end
|
|
|
|
|
2023-08-24 05:25:04 +08:00
|
|
|
defp render_output(
|
2023-08-24 04:39:36 +08:00
|
|
|
%{type: :error, context: {:interrupt, variant, message}},
|
2023-08-24 05:25:04 +08:00
|
|
|
%{cell_id: cell_id}
|
|
|
|
) do
|
2023-05-26 19:12:41 +08:00
|
|
|
assigns = %{variant: variant, message: message, cell_id: cell_id}
|
|
|
|
|
|
|
|
~H"""
|
|
|
|
<div class={[
|
|
|
|
"flex justify-between items-center px-4 py-2 border-l-4 shadow-custom-1",
|
|
|
|
case @variant do
|
|
|
|
:error -> "text-red-400 border-red-400"
|
|
|
|
:normal -> "text-gray-500 border-gray-300"
|
|
|
|
end
|
|
|
|
]}>
|
|
|
|
<div>
|
|
|
|
<%= @message %>
|
|
|
|
</div>
|
|
|
|
<button
|
|
|
|
class={[
|
2024-02-09 02:27:16 +08:00
|
|
|
"px-5 py-2 font-medium text-sm inline-flex rounded-lg border whitespace-nowrap items-center justify-center gap-1",
|
2023-05-26 19:12:41 +08:00
|
|
|
case @variant do
|
|
|
|
:error -> "border-red-400 text-red-400 hover:bg-red-50 focus:bg-red-50"
|
|
|
|
:normal -> "border-gray-300 text-gray-500 hover:bg-gray-100 focus:bg-gray-100"
|
|
|
|
end
|
|
|
|
]}
|
|
|
|
phx-click="queue_interrupted_cell_evaluation"
|
|
|
|
phx-value-cell_id={@cell_id}
|
|
|
|
>
|
2024-02-09 02:27:16 +08:00
|
|
|
<.remix_icon icon="play-circle-fill" />
|
2023-05-26 19:12:41 +08:00
|
|
|
<span>Continue</span>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
"""
|
|
|
|
end
|
|
|
|
|
2024-01-22 15:56:39 +08:00
|
|
|
defp render_output(%{type: :error, message: message}, %{id: id}) do
|
|
|
|
render_formatted_error_message(id, message)
|
2022-09-01 07:45:55 +08:00
|
|
|
end
|
|
|
|
|
2021-11-26 01:43:42 +08:00
|
|
|
defp render_output(output, %{}) do
|
2023-10-07 02:23:45 +08:00
|
|
|
req = Livebook.Runtime.Definitions.kino_requirement()
|
|
|
|
|
2022-01-26 04:55:24 +08:00
|
|
|
render_error_message("""
|
2023-10-07 02:23:45 +08:00
|
|
|
Unknown output format: #{inspect(output)}. You may want to explicitly \
|
|
|
|
add {:kino, "#{req}"} as a notebook dependency or update to the latest \
|
|
|
|
Livebook.
|
2021-11-09 03:45:30 +08:00
|
|
|
""")
|
|
|
|
end
|
|
|
|
|
2022-01-26 04:55:24 +08:00
|
|
|
defp render_error_message(message) do
|
2021-11-09 03:45:30 +08:00
|
|
|
assigns = %{message: message}
|
|
|
|
|
|
|
|
~H"""
|
2022-08-02 21:51:02 +08:00
|
|
|
<div
|
2022-10-07 18:00:39 +08:00
|
|
|
class="whitespace-pre-wrap break-words font-editor text-red-600"
|
2022-08-02 21:51:02 +08:00
|
|
|
role="complementary"
|
|
|
|
aria-label="error message"
|
2022-08-03 00:22:49 +08:00
|
|
|
phx-no-format
|
|
|
|
><%= @message %></div>
|
2021-11-09 03:45:30 +08:00
|
|
|
"""
|
|
|
|
end
|
2022-09-01 07:45:55 +08:00
|
|
|
|
2024-01-22 15:56:39 +08:00
|
|
|
defp render_formatted_error_message(id, message) do
|
|
|
|
assigns = %{id: id, message: message}
|
2022-09-01 07:45:55 +08:00
|
|
|
|
|
|
|
~H"""
|
2024-01-22 15:56:39 +08:00
|
|
|
<div id={@id} class="relative group/error">
|
|
|
|
<div
|
|
|
|
id={"#{@id}-message"}
|
|
|
|
class="whitespace-pre-wrap break-words font-editor text-gray-500"
|
|
|
|
role="complementary"
|
|
|
|
aria-label="error"
|
|
|
|
phx-no-format
|
2024-01-26 15:23:37 +08:00
|
|
|
><%= LivebookWeb.ANSIHelpers.ansi_string_to_html(@message) %></div>
|
2024-01-22 15:56:39 +08:00
|
|
|
<div class="absolute right-2 top-0 z-10 invisible group-hover/error:visible">
|
2024-02-09 02:27:16 +08:00
|
|
|
<.icon_button phx-click={JS.dispatch("lb:clipcopy", to: "##{@id}-message")}>
|
|
|
|
<.remix_icon icon="clipboard-line" />
|
|
|
|
</.icon_button>
|
2024-01-22 15:56:39 +08:00
|
|
|
</div>
|
|
|
|
</div>
|
2022-09-01 07:45:55 +08:00
|
|
|
"""
|
|
|
|
end
|
2021-11-09 03:45:30 +08:00
|
|
|
end
|