Skip to main content

yggdrasil/pages/admin/settings/
runner_section.rs

1//! 代码运行器配置分区(沙箱资源限制)。
2//!
3//! Tier B 可编辑+重启生效:RUNNER_CONFIG LazyLock 在首次访问时从启动配置构建。
4
5use dioxus::prelude::*;
6
7#[cfg(target_arch = "wasm32")]
8use crate::api::settings::{get_runner_settings, update_runner_settings};
9#[cfg(target_arch = "wasm32")]
10use crate::components::forms::{FormLabel, INPUT_CLASS};
11#[cfg(target_arch = "wasm32")]
12use crate::components::ui::{Checkbox, LoadingButton, ADMIN_CARD_CLASS};
13#[cfg(target_arch = "wasm32")]
14use crate::models::settings::RunnerSettings;
15
16/// 代码运行器配置分区组件。
17#[allow(non_snake_case)]
18#[component]
19pub fn RunnerSection(toast: Callback<(String, bool)>) -> Element {
20    #[cfg(target_arch = "wasm32")]
21    {
22        let mut saved: Signal<RunnerSettings> = use_signal(RunnerSettings::default);
23        let mut draft: Signal<RunnerSettings> = use_signal(RunnerSettings::default);
24        let loading = use_signal(|| true);
25        let mut saving = use_signal(|| false);
26        let mut just_saved = use_signal(|| false);
27
28        use_effect(move || {
29            #[cfg(target_arch = "wasm32")]
30            {
31                let mut saved = saved;
32                let mut draft = draft;
33                let mut loading = loading;
34                spawn(async move {
35                    match get_runner_settings().await {
36                        Ok(s) => {
37                            saved.set(s.clone());
38                            draft.set(s);
39                        }
40                        Err(e) => toast.call((format!("加载失败:{e}"), true)),
41                    }
42                    loading.set(false);
43                });
44            }
45        });
46
47        let dirty = use_memo(move || draft() != saved());
48
49        rsx! {
50            div { class: "space-y-6",
51                div { class: "{ADMIN_CARD_CLASS} p-6 md:p-8 flex flex-col gap-6",
52                    div { class: "flex items-center gap-3",
53                        span { class: "inline-flex items-center justify-center w-10 h-10 rounded-full bg-[var(--color-paper-theme)] text-[var(--color-paper-primary)] border border-[var(--color-paper-border)]",
54                            svg {
55                                class: "w-5 h-5",
56                                view_box: "0 0 24 24",
57                                fill: "none",
58                                stroke: "currentColor",
59                                stroke_width: "1.8",
60                                stroke_linecap: "round",
61                                stroke_linejoin: "round",
62                                path { d: "M16 18l6-6-6-6 M8 6l-6 6 6 6" }
63                            }
64                        }
65                        div {
66                            h2 { class: "text-xl font-bold text-[var(--color-paper-primary)]",
67                                "代码运行器"
68                            }
69                            p { class: "text-sm text-[var(--color-paper-secondary)] mt-0.5",
70                                span { class: "text-amber-600 dark:text-amber-400 font-medium",
71                                    "⚠ 修改后需重启服务生效。"
72                                }
73                            }
74                        }
75                    }
76
77                    label { class: "flex items-center gap-3 cursor-pointer max-w-xl",
78                        Checkbox {
79                            checked: draft().allow_network,
80                            onchange: move |checked: bool| {
81                                let mut d = draft();
82                                d.allow_network = checked;
83                                draft.set(d);
84                                just_saved.set(false);
85                            },
86                        }
87                        div { class: "flex flex-col",
88                            span { class: "text-sm font-medium text-[var(--color-paper-primary)]",
89                                "允许容器联网"
90                            }
91                            span { class: "text-xs text-[var(--color-paper-secondary)]",
92                                "⚠️ 启用后用户代码可访问网络,存在安全风险。"
93                            }
94                        }
95                    }
96
97                    NumberField {
98                        id: "runner-concurrent".to_string(),
99                        label: "最大并发任务数".to_string(),
100                        value: draft().max_concurrent.to_string(),
101                        oninput: move |v: String| {
102                            if let Ok(n) = v.parse::<u32>() {
103                                let mut d = draft();
104                                d.max_concurrent = n;
105                                draft.set(d);
106                                just_saved.set(false);
107                            }
108                        },
109                    }
110                    NumberField {
111                        id: "runner-cpu".to_string(),
112                        label: "每任务最大 CPU 核数".to_string(),
113                        value: format!("{:.1}", draft().max_cpu_cores),
114                        oninput: move |v: String| {
115                            if let Ok(n) = v.parse::<f64>() {
116                                let mut d = draft();
117                                d.max_cpu_cores = n;
118                                draft.set(d);
119                                just_saved.set(false);
120                            }
121                        },
122                        is_float: true,
123                    }
124                    NumberField {
125                        id: "runner-mem".to_string(),
126                        label: "每任务最大内存(MB)".to_string(),
127                        value: draft().max_memory_mb.to_string(),
128                        oninput: move |v: String| {
129                            if let Ok(n) = v.parse::<u32>() {
130                                let mut d = draft();
131                                d.max_memory_mb = n;
132                                draft.set(d);
133                                just_saved.set(false);
134                            }
135                        },
136                    }
137                    NumberField {
138                        id: "runner-timeout".to_string(),
139                        label: "每任务最大执行超时(秒)".to_string(),
140                        value: draft().max_timeout_secs.to_string(),
141                        oninput: move |v: String| {
142                            if let Ok(n) = v.parse::<u32>() {
143                                let mut d = draft();
144                                d.max_timeout_secs = n;
145                                draft.set(d);
146                                just_saved.set(false);
147                            }
148                        },
149                    }
150                    NumberField {
151                        id: "runner-output".to_string(),
152                        label: "每任务最大输出字节数".to_string(),
153                        value: draft().max_output_bytes.to_string(),
154                        oninput: move |v: String| {
155                            if let Ok(n) = v.parse::<u64>() {
156                                let mut d = draft();
157                                d.max_output_bytes = n;
158                                draft.set(d);
159                                just_saved.set(false);
160                            }
161                        },
162                    }
163                    NumberField {
164                        id: "runner-source".to_string(),
165                        label: "每任务最大源码字节数".to_string(),
166                        value: draft().max_source_bytes.to_string(),
167                        oninput: move |v: String| {
168                            if let Ok(n) = v.parse::<u64>() {
169                                let mut d = draft();
170                                d.max_source_bytes = n;
171                                draft.set(d);
172                                just_saved.set(false);
173                            }
174                        },
175                    }
176                    NumberField {
177                        id: "runner-queue".to_string(),
178                        label: "排队等待超时(秒)".to_string(),
179                        value: draft().queue_timeout_secs.to_string(),
180                        oninput: move |v: String| {
181                            if let Ok(n) = v.parse::<u32>() {
182                                let mut d = draft();
183                                d.queue_timeout_secs = n;
184                                draft.set(d);
185                                just_saved.set(false);
186                            }
187                        },
188                    }
189                    NumberField {
190                        id: "runner-ttl".to_string(),
191                        label: "历史 task 保留时长(秒)".to_string(),
192                        value: draft().task_ttl_secs.to_string(),
193                        oninput: move |v: String| {
194                            if let Ok(n) = v.parse::<u32>() {
195                                let mut d = draft();
196                                d.task_ttl_secs = n;
197                                draft.set(d);
198                                just_saved.set(false);
199                            }
200                        },
201                    }
202
203                    div { class: "flex flex-col gap-2 max-w-xl",
204                        FormLabel {
205                            label: "语言白名单(逗号分隔,留空=全部)",
206                            html_for: Some("runner-langs".to_string()),
207                        }
208                        input {
209                            id: "runner-langs",
210                            r#type: "text",
211                            class: "{INPUT_CLASS}",
212                            value: draft().languages.as_deref().unwrap_or(""),
213                            placeholder: "python,node,rust(留空=全部)",
214                            oninput: move |e: Event<FormData>| {
215                                let v = e.value();
216                                let mut d = draft();
217                                d.languages = if v.trim().is_empty() { None } else { Some(v) };
218                                draft.set(d);
219                                just_saved.set(false);
220                            },
221                        }
222                        p { class: "text-xs text-[var(--color-paper-secondary)]",
223                            "限制可用语言。留空表示全部注册语言可用。"
224                        }
225                    }
226
227                    div { class: "flex items-center justify-between gap-4 pt-1",
228                        if just_saved() {
229                            span { class: "inline-flex items-center gap-1.5 text-xs text-[var(--color-paper-accent)]",
230                                svg {
231                                    class: "w-3.5 h-3.5",
232                                    view_box: "0 0 24 24",
233                                    fill: "none",
234                                    stroke: "currentColor",
235                                    stroke_width: "2.5",
236                                    path {
237                                        stroke_linecap: "round",
238                                        stroke_linejoin: "round",
239                                        d: "M5 13l4 4L19 7",
240                                    }
241                                }
242                                "已保存(重启后生效)"
243                            }
244                        } else if dirty() {
245                            span { class: "text-xs text-[var(--color-paper-secondary)]",
246                                "有未保存的更改"
247                            }
248                        } else {
249                            span { class: "text-xs text-transparent select-none", "·" }
250                        }
251                        LoadingButton {
252                            label: "保存设置".to_string(),
253                            loading: saving(),
254                            disabled: loading() || just_saved() || !dirty(),
255                            onclick: move |_| {
256                                let d = draft();
257                                saving.set(true);
258                                spawn(async move {
259                                    match update_runner_settings(
260                                            d.allow_network,
261                                            d.max_concurrent,
262                                            d.max_cpu_cores,
263                                            d.max_memory_mb,
264                                            d.max_timeout_secs,
265                                            d.max_output_bytes,
266                                            d.max_source_bytes,
267                                            d.queue_timeout_secs,
268                                            d.task_ttl_secs,
269                                            d.languages,
270                                        )
271                                        .await
272                                    {
273                                        Ok(s) => {
274                                            saved.set(s.clone());
275                                            draft.set(s);
276                                            just_saved.set(true);
277                                            toast.call(("保存成功".to_string(), false));
278                                        }
279                                        Err(e) => toast.call((format!("保存失败:{e}"), true)),
280                                    }
281                                    saving.set(false);
282                                });
283                            },
284                        }
285                    }
286                }
287            }
288        }
289    }
290    #[cfg(not(target_arch = "wasm32"))]
291    {
292        rsx! {
293            div { class: "p-8 text-[var(--color-paper-secondary)]", "运行器配置(前端渲染)" }
294        }
295    }
296}
297#[cfg(target_arch = "wasm32")]
298/// 通用数字输入字段(支持整数和浮点)。
299#[allow(non_snake_case)]
300#[component]
301fn NumberField(
302    id: String,
303    label: String,
304    value: String,
305    oninput: Callback<String>,
306    #[props(default = false)] is_float: bool,
307) -> Element {
308    rsx! {
309        div { class: "flex flex-col gap-2 max-w-xl",
310            FormLabel { label: "{label}", html_for: Some(id.clone()) }
311            input {
312                id: "{id}",
313                r#type: "number",
314                step: if is_float { "0.1" } else { "1" },
315                class: "{INPUT_CLASS}",
316                value: "{value}",
317                oninput: move |e: Event<FormData>| oninput.call(e.value()),
318            }
319        }
320    }
321}