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
- 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 不进入本次合并
189 lines
6.1 KiB
Rust
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(())
|
|
}
|