wip: snapshot das alterações não commitadas (2026-10-20)
Origem: D:/Documents/Github/PiAgentUI/PiAgentUi Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 4
This commit is contained in:
1 parent
b3489e1ede
commit
98b5db029f
70 files changed
+9466
-9191
No files matched your search
@@ -335,7 +335,7 @@ async fn connect_ws(
|
||||
tokio::select! {
|
||||
outbound = rx.recv() => match outbound {
|
||||
Some(BridgeCommand::Send(data)) => {
|
||||
if let Err(error) = write.send(Message::Text(data.into())).await {
|
||||
if let Err(error) = write.send(Message::Text(data)).await {
|
||||
let msg = format!("WebSocket write failed: {}", error);
|
||||
emit(&on_event, BridgeEvent::Error { message: msg.clone() });
|
||||
state.remove_if_current(&key, conn_id);
|
||||
|
||||
@@ -4,16 +4,19 @@
|
||||
// ============================================
|
||||
|
||||
use crate::app::service::ServiceState;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs,
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
sync::atomic::Ordering,
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::State;
|
||||
use tauri::{Manager, State};
|
||||
|
||||
/// 检查 pi 服务是否在运行(通过 health endpoint)
|
||||
pub async fn is_service_running(url: &str) -> bool {
|
||||
let health_url = format!("{}/api/health", url.trim_end_matches('/'));
|
||||
let health_url = format!("{}/global/health", url.trim_end_matches('/'));
|
||||
match reqwest::Client::builder()
|
||||
.connect_timeout(Duration::from_secs(3))
|
||||
.build()
|
||||
@@ -29,23 +32,194 @@ pub async fn is_service_running(url: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动 pi serve 进程
|
||||
fn spawn_pi_serve(
|
||||
binary_path: &str,
|
||||
env_vars: &std::collections::HashMap<String, String>,
|
||||
) -> Result<std::process::Child, String> {
|
||||
log::info!("Starting pi serve with binary: {}", binary_path);
|
||||
if !env_vars.is_empty() {
|
||||
log::info!("Injecting {} environment variable(s)", env_vars.len());
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct PiAgentConnection {
|
||||
pub port: u16,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct PiAgentServiceStart {
|
||||
pub started: bool,
|
||||
pub connection: Option<PiAgentConnection>,
|
||||
pub agent_dir: String,
|
||||
pub session_dir: String,
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn e2e_config_value(key: &str) -> Option<String> {
|
||||
let config_path = std::env::current_exe()
|
||||
.ok()?
|
||||
.parent()?
|
||||
.join("piagentui-e2e.json");
|
||||
let config = fs::read_to_string(config_path).ok()?;
|
||||
let value: serde_json::Value = serde_json::from_str(&config).ok()?;
|
||||
value.get(key)?.as_str().map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
pub fn resolve_app_agent_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
|
||||
#[cfg(debug_assertions)]
|
||||
if let Ok(agent_dir) = std::env::var("PI_CODING_AGENT_DIR") {
|
||||
return Ok(PathBuf::from(agent_dir));
|
||||
}
|
||||
#[cfg(debug_assertions)]
|
||||
if let Some(agent_dir) = e2e_config_value("agentDir") {
|
||||
return Ok(PathBuf::from(agent_dir));
|
||||
}
|
||||
app.path()
|
||||
.app_data_dir()
|
||||
.map(|dir| dir.join("pi-runtime").join("agent"))
|
||||
.map_err(|e| format!("failed to resolve app data dir: {}", e))
|
||||
}
|
||||
|
||||
fn resolve_app_session_dir(app: &tauri::AppHandle) -> Result<PathBuf, String> {
|
||||
Ok(resolve_app_agent_dir(app)?.join("sessions"))
|
||||
}
|
||||
|
||||
pub fn read_app_connection(app: &tauri::AppHandle) -> Option<PiAgentConnection> {
|
||||
let conn_file = resolve_app_agent_dir(app).ok()?.join("piagentui-port.json");
|
||||
if !conn_file.exists() {
|
||||
return None;
|
||||
}
|
||||
let file = fs::File::open(conn_file).ok()?;
|
||||
serde_json::from_reader(file).ok()
|
||||
}
|
||||
|
||||
fn connection_url(connection: &PiAgentConnection) -> String {
|
||||
format!("http://127.0.0.1:{}", connection.port)
|
||||
}
|
||||
|
||||
fn resolve_app_extension_path(app: &tauri::AppHandle) -> Result<PathBuf, String> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let source_extension = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.map(|root| root.join("extensions").join("piagentui-server.ts"));
|
||||
if let Some(path) = source_extension {
|
||||
if path.exists() {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(binary_path);
|
||||
cmd.arg("rpc").stdout(Stdio::null()).stderr(Stdio::null());
|
||||
let resource_extension = app
|
||||
.path()
|
||||
.resource_dir()
|
||||
.map_err(|e| format!("failed to resolve app resource dir: {}", e))?
|
||||
.join("extensions")
|
||||
.join("piagentui-server.ts");
|
||||
|
||||
// 注入用户配置的环境变量
|
||||
if resource_extension.exists() {
|
||||
return Ok(resource_extension);
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"PiAgentUI bundled extension was not found at {}. The desktop app cannot expose provider onboarding APIs without it.",
|
||||
resource_extension.display()
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PiCommandSpec {
|
||||
program: PathBuf,
|
||||
prefix_args: Vec<String>,
|
||||
display: String,
|
||||
}
|
||||
|
||||
fn is_default_pi_binary(binary_path: &str) -> bool {
|
||||
let trimmed = binary_path.trim();
|
||||
trimmed.is_empty() || trimmed == "pi"
|
||||
}
|
||||
|
||||
fn resolve_pi_command(binary_path: &str) -> PiCommandSpec {
|
||||
let trimmed = binary_path.trim();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if is_default_pi_binary(trimmed) {
|
||||
if let Some(local_cli) = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.and_then(|root| root.parent().map(|workspace| workspace.join("pi").join("packages").join("coding-agent").join("dist").join("cli.js")))
|
||||
{
|
||||
if local_cli.exists() {
|
||||
let node = std::env::var("PIAGENTUI_NODE_BINARY").unwrap_or_else(|_| "node".to_string());
|
||||
return PiCommandSpec {
|
||||
program: PathBuf::from(node.clone()),
|
||||
prefix_args: vec![local_cli.display().to_string()],
|
||||
display: format!("{} {}", node, local_cli.display()),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let program = if trimmed.is_empty() { "pi" } else { trimmed };
|
||||
PiCommandSpec {
|
||||
program: PathBuf::from(program),
|
||||
prefix_args: Vec::new(),
|
||||
display: program.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动 pi RPC 进程 with an app-private Pi runtime.
|
||||
fn spawn_pi_rpc(
|
||||
app: &tauri::AppHandle,
|
||||
binary_path: &str,
|
||||
env_vars: &std::collections::HashMap<String, String>,
|
||||
) -> Result<(std::process::Child, PathBuf, PathBuf), String> {
|
||||
let agent_dir = resolve_app_agent_dir(app)?;
|
||||
let session_dir = resolve_app_session_dir(app)?;
|
||||
let extension_path = resolve_app_extension_path(app)?;
|
||||
let pi_command = resolve_pi_command(binary_path);
|
||||
fs::create_dir_all(&session_dir).map_err(|e| format!("failed to create private Pi runtime dir: {}", e))?;
|
||||
|
||||
log::info!("Starting isolated pi RPC backend with command: {}", pi_command.display);
|
||||
log::info!("Using private Pi agent dir: {}", agent_dir.display());
|
||||
log::info!("Loading PiAgentUI extension: {}", extension_path.display());
|
||||
if !env_vars.is_empty() {
|
||||
log::info!("Injecting {} user environment variable(s)", env_vars.len());
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let mut cmd = if let (Some(node_path), Some(script_path)) =
|
||||
(e2e_config_value("node"), e2e_config_value("script"))
|
||||
{
|
||||
log::info!("Using E2E Pi backend shim: {} {}", node_path, script_path);
|
||||
let mut cmd = Command::new(node_path);
|
||||
cmd.arg(script_path);
|
||||
if let Some(marker_dir) = e2e_config_value("markerDir") {
|
||||
cmd.env("PIAGENTUI_E2E_MARKER_DIR", marker_dir);
|
||||
}
|
||||
cmd
|
||||
} else if let Ok(script_path) = std::env::var("PIAGENTUI_E2E_PI_SCRIPT") {
|
||||
let node_path = std::env::var("PIAGENTUI_E2E_NODE").unwrap_or_else(|_| binary_path.to_string());
|
||||
log::info!("Using E2E Pi backend shim: {} {}", node_path, script_path);
|
||||
let mut cmd = Command::new(node_path);
|
||||
cmd.arg(script_path);
|
||||
cmd
|
||||
} else {
|
||||
let mut cmd = Command::new(&pi_command.program);
|
||||
cmd.args(&pi_command.prefix_args);
|
||||
cmd
|
||||
};
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
let mut cmd = {
|
||||
let mut cmd = Command::new(&pi_command.program);
|
||||
cmd.args(&pi_command.prefix_args);
|
||||
cmd
|
||||
};
|
||||
|
||||
// Inject user-configured environment first, then enforce private Pi runtime vars.
|
||||
for (key, value) in env_vars {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
cmd.arg("--mode")
|
||||
.arg("rpc")
|
||||
.arg("--extension")
|
||||
.arg(&extension_path)
|
||||
.env("PI_CODING_AGENT_DIR", &agent_dir)
|
||||
.env("PI_CODING_AGENT_SESSION_DIR", &session_dir)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
@@ -54,12 +228,19 @@ fn spawn_pi_serve(
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
|
||||
cmd.spawn().map_err(|e| {
|
||||
let child = cmd.spawn().map_err(|e| {
|
||||
let local_hint = if is_default_pi_binary(binary_path) {
|
||||
" The app also tried its development fallback. If you are running from source, build local Pi with `pnpm --dir pi/packages/coding-agent build`."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!(
|
||||
"Failed to start '{}': {}. Check that the path is correct.",
|
||||
binary_path, e
|
||||
"Failed to start '{}': {}. Check that the path points to the pi executable.{}",
|
||||
pi_command.display, e, local_hint
|
||||
)
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok((child, agent_dir, session_dir))
|
||||
}
|
||||
|
||||
/// 跨平台杀进程
|
||||
@@ -92,36 +273,64 @@ pub async fn check_pi_service(url: String) -> Result<bool, String> {
|
||||
Ok(is_service_running(&url).await)
|
||||
}
|
||||
|
||||
/// 启动 pi serve
|
||||
/// Start an isolated pi RPC backend for the desktop app.
|
||||
#[tauri::command]
|
||||
pub async fn start_pi_service(
|
||||
app: tauri::AppHandle,
|
||||
state: State<'_, ServiceState>,
|
||||
url: String,
|
||||
binary_path: String,
|
||||
env_vars: std::collections::HashMap<String, String>,
|
||||
) -> Result<bool, String> {
|
||||
if is_service_running(&url).await {
|
||||
log::info!("pi service already running at {}", url);
|
||||
return Ok(false);
|
||||
) -> Result<PiAgentServiceStart, String> {
|
||||
if let Some(connection) = read_app_connection(&app) {
|
||||
let connection_url = connection_url(&connection);
|
||||
if is_service_running(&connection_url).await {
|
||||
log::info!("isolated pi backend already running at {}", connection_url);
|
||||
let agent_dir = resolve_app_agent_dir(&app)?;
|
||||
let session_dir = resolve_app_session_dir(&app)?;
|
||||
return Ok(PiAgentServiceStart {
|
||||
started: false,
|
||||
connection: Some(connection),
|
||||
agent_dir: agent_dir.display().to_string(),
|
||||
session_dir: session_dir.display().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let child = spawn_pi_serve(&binary_path, &env_vars)?;
|
||||
if is_service_running(&url).await {
|
||||
log::info!("legacy pi service already running at {}", url);
|
||||
}
|
||||
|
||||
let (child, agent_dir, session_dir) = spawn_pi_rpc(&app, &binary_path, &env_vars)?;
|
||||
let pid = child.id();
|
||||
log::info!("Started pi serve, PID: {}", pid);
|
||||
log::info!("Started isolated pi RPC backend, PID: {}", pid);
|
||||
|
||||
state.child_pid.store(pid, Ordering::SeqCst);
|
||||
state.we_started.store(true, Ordering::SeqCst);
|
||||
|
||||
for _ in 0..30 {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
if is_service_running(&url).await {
|
||||
log::info!("pi service is ready at {}", url);
|
||||
return Ok(true);
|
||||
for _ in 0..60 {
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
if let Some(connection) = read_app_connection(&app) {
|
||||
let connection_url = connection_url(&connection);
|
||||
if is_service_running(&connection_url).await {
|
||||
log::info!("isolated pi backend is ready at {}", connection_url);
|
||||
return Ok(PiAgentServiceStart {
|
||||
started: true,
|
||||
connection: Some(connection),
|
||||
agent_dir: agent_dir.display().to_string(),
|
||||
session_dir: session_dir.display().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::warn!("pi service started but health check not passing yet");
|
||||
Ok(true)
|
||||
log::warn!("isolated pi backend started but discovery/health is not ready yet");
|
||||
Ok(PiAgentServiceStart {
|
||||
started: true,
|
||||
connection: read_app_connection(&app),
|
||||
agent_dir: agent_dir.display().to_string(),
|
||||
session_dir: session_dir.display().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 停止 pi serve
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::app::dir_state::OpenDirectoryState;
|
||||
use crate::app::commands::pi_agent::{read_app_connection, PiAgentConnection};
|
||||
use std::sync::Arc;
|
||||
use tauri::State;
|
||||
|
||||
@@ -25,21 +26,7 @@ pub fn desktop_window_ready(window: tauri::Window) -> Result<(), String> {
|
||||
crate::app::mark_window_ready(&window).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||
pub struct PiAgentConnection {
|
||||
pub port: u16,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_pi_agent_connection() -> Option<PiAgentConnection> {
|
||||
let home = std::env::var_os("USERPROFILE")
|
||||
.or_else(|| std::env::var_os("HOME"))
|
||||
.map(std::path::PathBuf::from)?;
|
||||
let conn_file = home.join(".pi").join("agent").join("piagentui-port.json");
|
||||
if !conn_file.exists() {
|
||||
return None;
|
||||
}
|
||||
let file = std::fs::File::open(conn_file).ok()?;
|
||||
serde_json::from_reader(file).ok()
|
||||
pub fn get_pi_agent_connection(app: tauri::AppHandle) -> Option<PiAgentConnection> {
|
||||
read_app_connection(&app)
|
||||
}
|
||||
+24
-13
@@ -23,6 +23,13 @@ use std::sync::Arc;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use tauri::Emitter;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn is_e2e_run() -> bool {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| exe.parent().map(|dir| dir.join("piagentui-e2e.json")))
|
||||
.is_some_and(|path| path.exists())
|
||||
}
|
||||
/// 从命令行参数中提取目录路径
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn extract_directory_from_args(args: &[String]) -> Option<String> {
|
||||
@@ -43,18 +50,20 @@ fn create_main_window(app: &tauri::AppHandle) -> Result<tauri::WebviewWindow, ta
|
||||
return Ok(window);
|
||||
}
|
||||
|
||||
let config = app
|
||||
.config()
|
||||
.app
|
||||
.windows
|
||||
.iter()
|
||||
.find(|window| window.label == "main")
|
||||
.cloned()
|
||||
.expect("main window config missing");
|
||||
let builder = configure_desktop_window_builder(tauri::WebviewWindowBuilder::new(
|
||||
app,
|
||||
"main",
|
||||
tauri::WebviewUrl::App("index.html".into()),
|
||||
))
|
||||
.title("Pi Agent")
|
||||
.inner_size(800.0, 600.0)
|
||||
.resizable(true)
|
||||
.fullscreen(false);
|
||||
|
||||
configure_desktop_window_builder(tauri::WebviewWindowBuilder::from_config(app, &config)?)
|
||||
.visible(false)
|
||||
.build()
|
||||
#[cfg(windows)]
|
||||
let builder = builder.disable_drag_drop_handler();
|
||||
|
||||
builder.visible(false).build()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
@@ -188,11 +197,13 @@ pub fn run() {
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
let main_window = create_main_window(&app.handle())?;
|
||||
let main_window = create_main_window(app.handle())?;
|
||||
finish_desktop_window_setup(&main_window);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
main_window.open_devtools();
|
||||
if !is_e2e_run() {
|
||||
main_window.open_devtools();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
|
||||
@@ -6,16 +6,17 @@
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build"
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"beforeBuildCommand": "pnpm build"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"create": false,
|
||||
"create": true,
|
||||
"title": "Pi Agent",
|
||||
"visible": true,
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"titleBarStyle": "Overlay",
|
||||
@@ -40,6 +41,13 @@
|
||||
"active": true,
|
||||
"targets": ["nsis", "dmg", "deb"],
|
||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/[email protected]", "icons/icon.icns", "icons/icon.ico"],
|
||||
"resources": {
|
||||
"../extensions/piagentui-server.ts": "extensions/piagentui-server.ts",
|
||||
"../extensions/piagentui-server-core.ts": "extensions/piagentui-server-core.ts",
|
||||
"../extensions/piagentui-settings-store.ts": "extensions/piagentui-settings-store.ts",
|
||||
"../extensions/piagentui-extension-manager.ts": "extensions/piagentui-extension-manager.ts",
|
||||
"../extensions/piagentui-git-manager.ts": "extensions/piagentui-git-manager.ts"
|
||||
},
|
||||
"windows": {
|
||||
"nsis": {
|
||||
"installerHooks": "./windows/hooks.nsi"
|
||||
|
||||
Reference in new issue
Block a user