@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 +3 -1
- package/README.md +12 -0
- package/package.json +1 -1
- package/src/commands.rs +8 -0
- package/src/host.rs +203 -44
- package/src/lib.rs +74 -21
- package/src/python_runner.rs +788 -0
- package/src/update.rs +295 -34
- package/src/host_tests.rs +0 -27
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
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(
|
|
170
|
-
|
|
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
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
183
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
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
|
-
"
|
|
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::{
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.
|
|
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 |_|
|
|
111
|
-
return
|
|
170
|
+
.show(move |_| exit_handle.exit(1));
|
|
171
|
+
return;
|
|
112
172
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|