Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/windows.rs
T
k88936 8968762274
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m57s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m47s
Project CI / Native shell tests (pull_request) Failing after 50s
Project CI / Frontend tests (pull_request) Failing after 1m50s
Project CI / Backend tests (pull_request) Successful in 4m45s
Project CI / Repository checks (pull_request) Successful in 2m1s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m38s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m21s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m31s
合并 origin/master:DirectProject 输入校验取回空白片段回归用例
- validation.rs 保留本分支的 content_has_meaningful_input 与 Skill/附件独立上限,空输入报错取回 master 的用户可见文案“聊天内容不能为空”
- wire.rs 取回 master 新增的 text_projection / user_input_rejects / valid_references / nonempty_text 四条回归用例,测试导入合并两侧
- resourceReferenceInput.test.tsx 取 master 的注释口径:草稿 text 投影会 trim,提交用的 content 保留 chip 后的分隔空格
- pitfalls.md 与《AGC聊天素材引用》改为「任意非文本 part 均算有效内容」,与四类引用的实际校验对齐
- 本地私有改动的 .env 不进入本次合并
2026-09-21 21:42:41 +08:00

189 lines
6.1 KiB
Rust

use super::*;
#[cfg(windows)]
fn configure_windows_background_std_command_impl(
command: &mut std::process::Command,
create_process_group: bool,
) {
use std::os::windows::process::CommandExt;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
command.creation_flags(
CREATE_NO_WINDOW
| if create_process_group {
CREATE_NEW_PROCESS_GROUP
} else {
0
},
);
}
#[cfg(windows)]
pub(crate) fn configure_windows_background_std_command(
command: &mut std::process::Command,
create_process_group: bool,
) {
configure_windows_background_std_command_impl(command, create_process_group);
}
#[cfg(not(windows))]
pub(crate) fn configure_windows_background_std_command(
_command: &mut std::process::Command,
_create_process_group: bool,
) {
}
pub(crate) fn new_windows_background_std_command<S>(program: S) -> std::process::Command
where
S: AsRef<std::ffi::OsStr>,
{
let mut command = std::process::Command::new(program);
configure_windows_background_std_command(&mut command, false);
command
}
pub(crate) fn configure_windows_background_tokio_command(
command: &mut tokio::process::Command,
create_process_group: bool,
) {
configure_windows_background_std_command(command.as_std_mut(), create_process_group);
}
#[cfg(all(test, windows))]
mod windows_background_command_tests {
use super::*;
use std::fs;
use std::process::Stdio;
const CHILD_ENV: &str = "GENARRATIVE_WINDOWS_NO_CONSOLE_CHILD";
const RESULT_ENV: &str = "GENARRATIVE_WINDOWS_NO_CONSOLE_RESULT";
const FIXTURE_TEST: &str =
"windows::windows_background_command_tests::background_command_console_fixture";
#[test]
#[ignore = "child-process fixture"]
fn background_command_console_fixture() {
if std::env::var_os(CHILD_ENV).is_none() {
return;
}
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetConsoleWindow() -> windows_sys::Win32::Foundation::HWND;
}
let result_path = std::env::var_os(RESULT_ENV).expect("result path");
let has_console_window = unsafe { !GetConsoleWindow().is_null() };
fs::write(
result_path,
if has_console_window {
"console"
} else {
"hidden"
},
)
.expect("write console-window result");
}
#[test]
fn background_std_command_does_not_allocate_a_console_window() {
let directory = tempfile::tempdir().expect("create no-console test directory");
let result_path = directory.path().join("console-window.txt");
let mut command = new_windows_background_std_command(
std::env::current_exe().expect("current test binary"),
);
command
.args(["--exact", FIXTURE_TEST, "--ignored"])
.env(CHILD_ENV, "1")
.env(RESULT_ENV, &result_path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
let status = command.status().expect("run no-console child fixture");
assert!(status.success(), "no-console child fixture must succeed");
assert_eq!(
fs::read_to_string(result_path).expect("read console-window result"),
"hidden",
"CREATE_NO_WINDOW must keep background command probes from flashing a console window"
);
}
}
pub(crate) fn workspace_window_url(project_path: &str) -> tauri::WebviewUrl {
tauri::WebviewUrl::App(PathBuf::from(format!(
"index.html?main&projectPath={}",
percent_encode_query_value(project_path)
)))
}
pub(crate) fn launcher_window_url() -> tauri::WebviewUrl {
tauri::WebviewUrl::App(PathBuf::from("index.html?launcher"))
}
pub(crate) fn validate_workspace_window_project_path(project_path: &str) -> Result<&str, String> {
let project_path = project_path.trim();
if project_path.is_empty() {
return Err("请提供工作区绝对路径".to_string());
}
if !Path::new(project_path).is_absolute() {
return Err("工作区路径必须是绝对路径".to_string());
}
if project_path.chars().any(char::is_control) {
return Err("工作区路径不能包含控制字符".to_string());
}
Ok(project_path)
}
fn percent_encode_query_value(value: &str) -> String {
let mut encoded = String::new();
for byte in value.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
encoded.push(byte as char)
}
_ => encoded.push_str(&format!("%{byte:02X}")),
}
}
encoded
}
#[tauri::command]
pub(crate) fn open_game_creator_workspace_window(
app: tauri::AppHandle,
window: tauri::Window,
project_path: String,
) -> Result<(), String> {
let project_path = validate_workspace_window_project_path(&project_path)?;
if let Some(existing) = app.get_webview_window("main") {
existing.close().map_err(|error| error.to_string())?;
}
tauri::WebviewWindowBuilder::new(&app, "main", workspace_window_url(project_path))
.title(crate::game_creator_product_name(&app))
.decorations(false)
.inner_size(1180.0, 820.0)
.min_inner_size(760.0, 560.0)
.build()
.map_err(|error| error.to_string())?;
window.close().map_err(|error| error.to_string())?;
Ok(())
}
#[tauri::command]
pub(crate) fn open_game_creator_launcher_window(
app: tauri::AppHandle,
window: tauri::Window,
) -> Result<(), String> {
if let Some(existing) = app.get_webview_window("launcher") {
existing.set_focus().map_err(|error| error.to_string())?;
} else {
tauri::WebviewWindowBuilder::new(&app, "launcher", launcher_window_url())
.title(crate::game_creator_product_name(&app))
.decorations(false)
.inner_size(820.0, 640.0)
.min_inner_size(720.0, 520.0)
.build()
.map_err(|error| error.to_string())?;
}
window.close().map_err(|error| error.to_string())?;
Ok(())
}