@steerable/agent-shell-tauri 0.6.47 → 0.6.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Cargo.toml CHANGED
@@ -17,10 +17,12 @@ command-group = "5.0.1"
17
17
  image = { version = "0.25", default-features = false, features = ["png"] }
18
18
  serde = { version = "1", features = ["derive"] }
19
19
  serde_json = "1"
20
+ sha2 = "0.10"
21
+ hex = "0.4"
20
22
  tauri = { version = "2.11.6", features = [] }
21
23
  tauri-plugin-dialog = "2.7.3"
22
24
  tauri-plugin-process = "2.3.1"
23
25
  tauri-plugin-single-instance = "2.4.5"
24
26
  tauri-plugin-updater = "2.12.0"
25
- tokio = { version = "1", features = ["time"] }
27
+ tokio = { version = "1", features = ["io-util", "macros", "process", "sync", "time"] }
26
28
  url = "2"
package/README.md CHANGED
@@ -12,3 +12,15 @@ resource paths, and updates.
12
12
 
13
13
  The crate is source-distributed with the npm package and is not published to
14
14
  crates.io.
15
+
16
+ Packaged products place the complete portable Python sidecar at
17
+ `python-runtime/<platform>` and the verified egress proxy at
18
+ `engine/steerable-egress-proxy`. The Python runtime contains the native Rust
19
+ CoreLoop engine. An optional `engine/python-runner` is only a child interpreter
20
+ for `run_code`; it never hosts the sidecar. Products may set `pythonRunner` to `bundle` or `download`
21
+ in `product.json`. Download mode starts the product without an interpreter and
22
+ lets the settings UI install one in the background. The default
23
+ python-build-standalone archive is checked against the packaged SHA-256; users
24
+ may instead choose an unverified custom archive URL or an existing Python 3.9+
25
+ executable. Download progress is emitted to the renderer, and the selected
26
+ interpreter is passed as `STEERABLE_PYTHON` on the next application start.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell-tauri",
3
- "version": "0.6.47",
3
+ "version": "0.6.49",
4
4
  "description": "Reusable Tauri Rust host source for @steerable/agent-shell",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://steerableframework.com/",
package/src/commands.rs CHANGED
@@ -108,6 +108,14 @@ pub async fn host_save_text_file(
108
108
  })
109
109
  }
110
110
 
111
+ #[tauri::command]
112
+ pub fn host_read_clipboard_text() -> String {
113
+ let Ok(mut clipboard) = Clipboard::new() else {
114
+ return String::new();
115
+ };
116
+ clipboard.get_text().unwrap_or_default()
117
+ }
118
+
111
119
  #[tauri::command]
112
120
  pub async fn host_capture_screenshot(
113
121
  image: ScreenshotImage,
package/src/host.rs CHANGED
@@ -1,10 +1,11 @@
1
+ use crate::python_runner::{self, PythonRunnerLock, RunnerSetup};
1
2
  use crate::DesktopConfig;
2
3
  use command_group::{CommandGroup, GroupChild};
3
4
  #[cfg(unix)]
4
5
  use command_group::{Signal, UnixChildExt};
5
- use serde::Deserialize;
6
+ use serde::{Deserialize, Serialize};
6
7
  use std::env;
7
- use std::io::{BufRead, BufReader};
8
+ use std::io::{BufRead, BufReader, Write};
8
9
  use std::path::{Path, PathBuf};
9
10
  use std::process::{Command, Stdio};
10
11
  use std::sync::mpsc;
@@ -12,23 +13,58 @@ use std::sync::Mutex;
12
13
  use std::thread;
13
14
  use std::time::{Duration, Instant};
14
15
  use tauri::{AppHandle, Manager};
16
+ use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
15
17
  use url::Url;
16
18
 
17
19
  const READY_PREFIX: &str = "STEERABLE_HOST_READY ";
18
20
  const START_TIMEOUT: Duration = Duration::from_secs(30);
19
21
 
20
- #[derive(Deserialize)]
22
+ #[derive(Deserialize, Serialize)]
21
23
  struct ReadyRecord {
22
24
  host: String,
23
25
  port: u16,
24
26
  }
25
27
 
28
+ #[derive(Deserialize)]
29
+ #[serde(rename_all = "camelCase")]
30
+ struct ProductRuntime {
31
+ python_runner: Option<String>,
32
+ }
33
+
34
+ fn write_diag(dir: &Path, name: &str, body: &str) {
35
+ let _ = std::fs::write(dir.join(name), body);
36
+ }
37
+
38
+ fn append_diag(dir: &Path, name: &str, line: &str) {
39
+ if let Ok(mut file) = std::fs::OpenOptions::new()
40
+ .create(true)
41
+ .append(true)
42
+ .open(dir.join(name))
43
+ {
44
+ let _ = writeln!(file, "{line}");
45
+ }
46
+ }
47
+
26
48
  pub struct HostProcess {
27
49
  child: Mutex<Option<GroupChild>>,
28
50
  }
29
51
 
30
52
  impl HostProcess {
31
53
  pub fn spawn(app: &AppHandle, config: &DesktopConfig) -> Result<(Self, Url), String> {
54
+ match Self::spawn_inner(app, config) {
55
+ Ok(started) => Ok(started),
56
+ Err(error) => {
57
+ if let Some(dir) = env::var_os("DEEPPATH_USER_DATA_DIR") {
58
+ let dir = PathBuf::from(dir);
59
+ let _ = std::fs::create_dir_all(&dir);
60
+ write_diag(&dir, "host-error.txt", &error);
61
+ }
62
+ Err(error)
63
+ }
64
+ }
65
+ }
66
+
67
+ fn spawn_inner(app: &AppHandle, config: &DesktopConfig) -> Result<(Self, Url), String> {
32
68
  let paths = HostPaths::resolve(app, config)?;
33
69
  let user_data = match env::var_os("DEEPPATH_USER_DATA_DIR") {
34
70
  Some(path) => PathBuf::from(path),
@@ -39,6 +75,30 @@ impl HostProcess {
39
75
  .join(&config.data_dir_name),
40
76
  };
41
77
  std::fs::create_dir_all(&user_data).map_err(|error| error.to_string())?;
78
+ let sidecar_tmp = user_data.join("sidecar/tmp");
79
+ std::fs::create_dir_all(&sidecar_tmp).map_err(|error| error.to_string())?;
80
+ let python_runner = paths.resolve_python_runner(app, config, &user_data);
81
+ if let Err(error) = &python_runner {
82
+ eprintln!("[python-runner] {error}; run_code will be unavailable");
83
+ app.dialog()
84
+ .message(format!(
85
+ "Python 运行器安装失败,run_code 本次不可用。\n\n{error}"
86
+ ))
87
+ .title("Python 运行器")
88
+ .kind(MessageDialogKind::Error)
89
+ .show(|_| {});
90
+ }
91
+ write_diag(
92
+ &user_data,
93
+ "host-spawn.txt",
94
+ &format!(
95
+ "node={}\nserver={}\napp_root={}\nweb_dist={}\n",
96
+ paths.node.display(),
97
+ paths.server_entry.display(),
98
+ paths.app_root.display(),
99
+ paths.web_dist.display()
100
+ ),
101
+ );
42
102
 
43
103
  let mut command = Command::new(&paths.node);
44
104
  command
@@ -49,12 +109,15 @@ impl HostProcess {
49
109
  .env("DEEPPATH_BS_HOST", "127.0.0.1")
50
110
  .env("DEEPPATH_BS_PORT", "0")
51
111
  .env("DEEPPATH_WEB_DIST", &paths.web_dist)
52
- .env("DEEPPATH_USER_DATA_DIR", user_data)
112
+ .env("DEEPPATH_USER_DATA_DIR", &user_data)
113
+ .env("TMPDIR", &sidecar_tmp)
114
+ .env("TMP", &sidecar_tmp)
115
+ .env("TEMP", &sidecar_tmp)
53
116
  .env("STEERABLE_HOST_PARENT_PID", std::process::id().to_string())
54
117
  .stdin(Stdio::null())
55
118
  .stdout(Stdio::piped())
56
119
  .stderr(Stdio::piped());
57
- paths.apply_runtime_env(&mut command);
120
+ paths.apply_runtime_env(&mut command, python_runner.ok().flatten());
58
121
 
59
122
  let mut child = spawn_group(&mut command).map_err(|error| {
60
123
  format!(
@@ -74,11 +137,13 @@ impl HostProcess {
74
137
  .ok_or_else(|| "Node host stderr was not piped".to_string())?;
75
138
 
76
139
  let (ready_tx, ready_rx) = mpsc::channel();
140
+ let stdout_dir = user_data.clone();
77
141
  thread::spawn(move || {
78
142
  for line in BufReader::new(stdout).lines() {
79
143
  match line {
80
144
  Ok(line) => {
81
145
  println!("[node-host] {line}");
146
+ append_diag(&stdout_dir, "host-node.log", &line);
82
147
  if let Some(record) = line.strip_prefix(READY_PREFIX) {
83
148
  let parsed = serde_json::from_str::<ReadyRecord>(record)
84
149
  .map_err(|error| error.to_string());
@@ -86,15 +151,22 @@ impl HostProcess {
86
151
  }
87
152
  }
88
153
  Err(error) => {
154
+ append_diag(
155
+ &stdout_dir,
156
+ "host-node.log",
157
+ &format!("stdout error: {error}"),
158
+ );
89
159
  let _ = ready_tx.send(Err(error.to_string()));
90
160
  break;
91
161
  }
92
162
  }
93
163
  }
94
164
  });
165
+ let stderr_dir = user_data.clone();
95
166
  thread::spawn(move || {
96
167
  for line in BufReader::new(stderr).lines().map_while(Result::ok) {
97
168
  eprintln!("[node-host] {line}");
169
+ append_diag(&stderr_dir, "host-node.log", &line);
98
170
  }
99
171
  });
100
172
 
@@ -118,6 +190,11 @@ impl HostProcess {
118
190
  return Err("Node host did not become ready within 30 seconds".to_string());
119
191
  }
120
192
  };
193
+ write_diag(
194
+ &user_data,
195
+ "host-ready.json",
196
+ &serde_json::to_string(&ready).map_err(|error| error.to_string())?,
197
+ );
121
198
  let url = Url::parse(&format!("http://{}:{}/", ready.host, ready.port))
122
199
  .map_err(|error| error.to_string())?;
123
200
  Ok((
@@ -162,39 +239,110 @@ struct HostPaths {
162
239
  app_root: PathBuf,
163
240
  web_dist: PathBuf,
164
241
  engine_dir: Option<PathBuf>,
242
+ sidecar_python: Option<PathBuf>,
165
243
  }
166
244
 
167
245
  impl HostPaths {
168
- fn apply_runtime_env(&self, command: &mut Command) {
169
- let Some(engine_dir) = &self.engine_dir else {
170
- return;
171
- };
172
- let rust_sidecar = engine_dir.join(platform_binary("steerable-sidecar"));
173
- if rust_sidecar.exists() {
174
- command
175
- .env("STEERABLE_RUST_SIDECAR", "1")
176
- .env("STEERABLE_RUST_SIDECAR_BIN", rust_sidecar);
246
+ fn apply_runtime_env(&self, command: &mut Command, python_runner: Option<PathBuf>) {
247
+ if let Some(sidecar_python) = &self.sidecar_python {
248
+ command.env("STEERABLE_SIDECAR_PYTHON", sidecar_python);
177
249
  }
178
- let egress_proxy = engine_dir.join(platform_binary("steerable-egress-proxy"));
179
- if egress_proxy.exists() {
180
- command.env("STEERABLE_EGRESS_PROXY_BIN", egress_proxy);
250
+ if let Some(engine_dir) = &self.engine_dir {
251
+ let egress_proxy = engine_dir.join(platform_binary("steerable-egress-proxy"));
252
+ if egress_proxy.exists() {
253
+ command.env("STEERABLE_EGRESS_PROXY_BIN", egress_proxy);
254
+ }
255
+ let win_spawn_helper = engine_dir.join("win-spawn-helper/win-spawn-helper.exe");
256
+ if win_spawn_helper.exists() {
257
+ command.env("DEEPPATH_WIN_SPAWN_HELPER", win_spawn_helper);
258
+ }
181
259
  }
182
- let python =
183
- engine_dir
184
- .join("python-runtime")
185
- .join(platform_tag())
186
- .join(if cfg!(windows) {
187
- "python/python.exe"
188
- } else {
189
- "python/bin/python3"
190
- });
191
- if python.exists() {
192
- command.env("STEERABLE_SIDECAR_PYTHON", python);
260
+ if let Some(python_runner) = python_runner {
261
+ command.env("STEERABLE_PYTHON", python_runner);
193
262
  }
194
- let win_spawn_helper = engine_dir.join("win-spawn-helper/win-spawn-helper.exe");
195
- if win_spawn_helper.exists() {
196
- command.env("DEEPPATH_WIN_SPAWN_HELPER", win_spawn_helper);
263
+ }
264
+
265
+ fn resolve_python_runner(
266
+ &self,
267
+ app: &AppHandle,
268
+ config: &DesktopConfig,
269
+ user_data: &Path,
270
+ ) -> Result<Option<PathBuf>, String> {
271
+ if let Some(explicit) = env::var_os("STEERABLE_PYTHON") {
272
+ let explicit = PathBuf::from(explicit);
273
+ if explicit.is_absolute() && explicit.is_file() {
274
+ python_runner::configure(
275
+ app,
276
+ RunnerSetup {
277
+ user_data: user_data.to_path_buf(),
278
+ node: self.node.clone(),
279
+ engine_dir: self.engine_dir.clone(),
280
+ lock: None,
281
+ target_name: platform_tag().into(),
282
+ supported: false,
283
+ active_runner: Some(explicit.clone()),
284
+ },
285
+ );
286
+ return Ok(Some(explicit));
287
+ }
288
+ return Err("STEERABLE_PYTHON must name an existing absolute file".into());
197
289
  }
290
+ let Some(engine_dir) = &self.engine_dir else {
291
+ return Ok(None);
292
+ };
293
+ let product: ProductRuntime = serde_json::from_str(
294
+ &std::fs::read_to_string(
295
+ self.app_root
296
+ .join("products")
297
+ .join(&config.product_id)
298
+ .join("product.json"),
299
+ )
300
+ .map_err(|error| error.to_string())?,
301
+ )
302
+ .map_err(|error| error.to_string())?;
303
+ let Some(python_runner_mode) = product.python_runner.as_deref() else {
304
+ return Ok(None);
305
+ };
306
+ let lock: PythonRunnerLock = serde_json::from_str(
307
+ &std::fs::read_to_string(engine_dir.join("python-runner-lock.json"))
308
+ .map_err(|error| error.to_string())?,
309
+ )
310
+ .map_err(|error| error.to_string())?;
311
+ let target_name = platform_tag();
312
+ let target = lock
313
+ .targets
314
+ .get(target_name)
315
+ .ok_or_else(|| format!("Python runner does not support {target_name}"))?;
316
+
317
+ let active_runner = if python_runner_mode == "bundle" {
318
+ let runner = engine_dir.join("python-runner").join(&target.runner);
319
+ runner
320
+ .is_file()
321
+ .then_some(runner)
322
+ .ok_or_else(|| "bundled Python runner is missing".to_string())?
323
+ } else if python_runner_mode == "download" {
324
+ python_runner::configured_runner(user_data)
325
+ .or_else(|| python_runner::default_runner(user_data, &lock, target_name, target))
326
+ .unwrap_or_default()
327
+ } else {
328
+ return Err(format!(
329
+ "unsupported pythonRunner mode {python_runner_mode:?}"
330
+ ));
331
+ };
332
+ let active_runner = (!active_runner.as_os_str().is_empty()).then_some(active_runner);
333
+ python_runner::configure(
334
+ app,
335
+ RunnerSetup {
336
+ user_data: user_data.to_path_buf(),
337
+ node: self.node.clone(),
338
+ engine_dir: self.engine_dir.clone(),
339
+ lock: Some(lock),
340
+ target_name: target_name.into(),
341
+ supported: python_runner_mode == "download",
342
+ active_runner: active_runner.clone(),
343
+ },
344
+ );
345
+ Ok(active_runner)
198
346
  }
199
347
 
200
348
  fn resolve(app: &AppHandle, config: &DesktopConfig) -> Result<Self, String> {
@@ -217,6 +365,7 @@ impl HostPaths {
217
365
  }),
218
366
  app_root,
219
367
  engine_dir: None,
368
+ sidecar_python: env::var_os("STEERABLE_SIDECAR_PYTHON").map(PathBuf::from),
220
369
  });
221
370
  }
222
371
 
@@ -227,17 +376,30 @@ impl HostPaths {
227
376
  let resource_dir = node_compatible_path(&resource_dir);
228
377
  let host_root = resource_dir.join("node-host");
229
378
  let app_root = host_root.join("app-dist");
230
- let engine_node = resource_dir.join("engine").join(platform_binary("node"));
379
+ let python_name = if cfg!(windows) {
380
+ "python.exe"
381
+ } else {
382
+ "python3"
383
+ };
384
+ let python_root = resource_dir.join("python-runtime").join(platform_tag());
385
+ let sidecar_python = [
386
+ python_root.join("python").join(python_name),
387
+ python_root.join("python/bin").join(python_name),
388
+ python_root.join(python_name),
389
+ python_root.join("bin").join(python_name),
390
+ ]
391
+ .into_iter()
392
+ .find(|candidate| candidate.is_file())
393
+ .ok_or_else(|| {
394
+ format!(
395
+ "packaged Python sidecar runtime is missing under {}",
396
+ python_root.display()
397
+ )
398
+ })?;
231
399
  Ok(Self {
232
400
  node: env::var_os("DEEPPATH_TAURI_NODE")
233
401
  .map(PathBuf::from)
234
- .unwrap_or_else(|| {
235
- if engine_node.exists() {
236
- engine_node
237
- } else {
238
- resource_dir.join(node_resource_name())
239
- }
240
- }),
402
+ .unwrap_or_else(|| resource_dir.join(node_resource_name())),
241
403
  server_entry: app_root
242
404
  .join("products")
243
405
  .join(&config.product_id)
@@ -245,6 +407,7 @@ impl HostPaths {
245
407
  web_dist: host_root.join("web-dist"),
246
408
  app_root,
247
409
  engine_dir: Some(resource_dir.join("engine")),
410
+ sidecar_python: Some(sidecar_python),
248
411
  })
249
412
  }
250
413
  }
@@ -273,7 +436,7 @@ fn platform_tag() -> &'static str {
273
436
 
274
437
  fn node_resource_name() -> &'static str {
275
438
  if cfg!(windows) {
276
- "node/node.exe"
439
+ "engine/node.exe"
277
440
  } else {
278
441
  "node/node"
279
442
  }
@@ -310,7 +473,3 @@ fn spawn_group(command: &mut Command) -> std::io::Result<GroupChild> {
310
473
  command.group_spawn()
311
474
  }
312
475
  }
313
-
314
- #[cfg(test)]
315
- #[path = "host_tests.rs"]
316
- mod tests;
package/src/lib.rs CHANGED
@@ -1,15 +1,24 @@
1
1
  mod commands;
2
2
  mod host;
3
+ mod python_runner;
3
4
  mod update;
4
5
 
5
- use commands::{host_capture_screenshot, host_save_text_file, host_select_directory};
6
+ use commands::{
7
+ host_capture_screenshot, host_read_clipboard_text, host_save_text_file, host_select_directory,
8
+ };
6
9
  use host::HostProcess;
10
+ use python_runner::{
11
+ python_runner_cancel, python_runner_download, python_runner_pick_local, python_runner_restart,
12
+ python_runner_snapshot, python_runner_use_default, python_runner_use_local,
13
+ };
7
14
  use std::path::PathBuf;
15
+ use std::thread;
8
16
  use tauri::{
9
17
  menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder},
10
18
  Emitter, Manager, RunEvent, WebviewUrl, WebviewWindowBuilder, WindowEvent,
11
19
  };
12
20
  use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
21
+ use update::{app_release_check, app_release_install, app_release_snapshot};
13
22
 
14
23
  /// Product values required by the reusable desktop host.
15
24
  pub struct DesktopConfig {
@@ -42,18 +51,28 @@ fn install_menu(app: &tauri::App) -> tauri::Result<()> {
42
51
  let open_terminal = MenuItemBuilder::with_id("open-terminal", "打开终端")
43
52
  .accelerator("CmdOrCtrl+T")
44
53
  .build(app)?;
54
+ let quit = MenuItemBuilder::with_id("app-quit", "退出")
55
+ .accelerator("CmdOrCtrl+Q")
56
+ .build(app)?;
45
57
  let file = SubmenuBuilder::new(app, "文件")
46
58
  .item(&new_chat)
47
59
  .separator()
48
60
  .close_window()
61
+ .separator()
62
+ .item(&quit)
49
63
  .build()?;
64
+ // Predefined Paste delivers an empty clipboard into WKWebView. Own the
65
+ // shortcut and read the pasteboard from the host process instead.
66
+ let paste = MenuItemBuilder::with_id("edit-paste", "粘贴")
67
+ .accelerator("CmdOrCtrl+V")
68
+ .build(app)?;
50
69
  let edit = SubmenuBuilder::new(app, "编辑")
51
70
  .undo()
52
71
  .redo()
53
72
  .separator()
54
73
  .cut()
55
74
  .copy()
56
- .paste()
75
+ .item(&paste)
57
76
  .select_all()
58
77
  .build()?;
59
78
  let view = SubmenuBuilder::new(app, "视图")
@@ -72,6 +91,10 @@ fn install_menu(app: &tauri::App) -> tauri::Result<()> {
72
91
  "open-terminal" => {
73
92
  let _ = app.emit("menu:open-terminal", ());
74
93
  }
94
+ "edit-paste" => {
95
+ let _ = app.emit("menu:paste", ());
96
+ }
97
+ "app-quit" => app.exit(0),
75
98
  _ => {}
76
99
  });
77
100
  Ok(())
@@ -82,6 +105,7 @@ pub fn run(context: tauri::Context<tauri::Wry>, config: DesktopConfig) {
82
105
  let product_name = config.product_name.clone();
83
106
  let builder = tauri::Builder::default()
84
107
  .manage(update::UpdateState::default())
108
+ .manage(python_runner::PythonRunnerState::default())
85
109
  .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
86
110
  if let Some(window) = app.get_webview_window("main") {
87
111
  let _ = window.show();
@@ -95,31 +119,60 @@ pub fn run(context: tauri::Context<tauri::Wry>, config: DesktopConfig) {
95
119
  .invoke_handler(tauri::generate_handler![
96
120
  host_select_directory,
97
121
  host_save_text_file,
98
- host_capture_screenshot
122
+ host_capture_screenshot,
123
+ host_read_clipboard_text,
124
+ app_release_snapshot,
125
+ app_release_check,
126
+ app_release_install,
127
+ python_runner_snapshot,
128
+ python_runner_download,
129
+ python_runner_cancel,
130
+ python_runner_pick_local,
131
+ python_runner_use_local,
132
+ python_runner_use_default,
133
+ python_runner_restart
99
134
  ])
100
135
  .setup(move |app| {
101
136
  install_menu(app)?;
102
- let (host, url) = match HostProcess::spawn(app.handle(), &config) {
103
- Ok(started) => started,
104
- Err(error) => {
105
- let handle = app.handle().clone();
106
- app.dialog()
107
- .message(error)
137
+ let handle = app.handle().clone();
138
+ thread::spawn(move || {
139
+ let (host, url) = match HostProcess::spawn(&handle, &config) {
140
+ Ok(started) => started,
141
+ Err(error) => {
142
+ let exit_handle = handle.clone();
143
+ handle
144
+ .dialog()
145
+ .message(error)
146
+ .title(format!("{} 无法启动", config.product_name))
147
+ .kind(MessageDialogKind::Error)
148
+ .show(move |_| exit_handle.exit(1));
149
+ return;
150
+ }
151
+ };
152
+ handle.manage(host);
153
+ let allowed_origin = url.origin().ascii_serialization();
154
+ if let Err(error) =
155
+ WebviewWindowBuilder::new(&handle, "main", WebviewUrl::External(url))
156
+ .title(&config.product_name)
157
+ .inner_size(1200.0, 800.0)
158
+ .min_inner_size(900.0, 650.0)
159
+ .on_navigation(move |url| {
160
+ url.origin().ascii_serialization() == allowed_origin
161
+ })
162
+ .build()
163
+ {
164
+ let exit_handle = handle.clone();
165
+ handle
166
+ .dialog()
167
+ .message(error.to_string())
108
168
  .title(format!("{} 无法启动", config.product_name))
109
169
  .kind(MessageDialogKind::Error)
110
- .show(move |_| handle.exit(1));
111
- return Ok(());
170
+ .show(move |_| exit_handle.exit(1));
171
+ return;
112
172
  }
113
- };
114
- app.manage(host);
115
- let allowed_origin = url.origin().ascii_serialization();
116
- WebviewWindowBuilder::new(app, "main", WebviewUrl::External(url))
117
- .title(&config.product_name)
118
- .inner_size(1200.0, 800.0)
119
- .min_inner_size(900.0, 650.0)
120
- .on_navigation(move |url| url.origin().ascii_serialization() == allowed_origin)
121
- .build()?;
122
- update::start(app.handle());
173
+ python_runner::maybe_prompt(&handle);
174
+ update::start(&handle);
175
+ });
123
176
  Ok(())
124
177
  });
125
178