dev-manager-tauri/src-tauri/src/lib.rs
lanrtop 97578b2261
All checks were successful
Push & PR Check / check (push) Successful in 1m27s
Push & PR Check / check (pull_request) Successful in 1m6s
feat(onboard/parity): 一键接入 MCP 与 Tauri 投影、Codex 信任计 pending_user 与仓库布局说明(P8e)
- MCP 新增 plan_onboarding(readOnly)与 onboard_project(幂等写操作):schema 各自单独成函数,
  dispatch 一行委托到 mcp_inject 生产入口(中间只做 JSON 类型解析);全局与项目路由都可见,
  项目路由强制本项目、跨项目返回 scope_violation;onboard_project required 含 expected_digest
  与 confirmed;共用可选选项 bypass_permissions / skip_targets;进 STRUCTURED_TOOLS 并给一行摘要
- Tauri 新增同名 async 命令(async fn + spawn_blocking),commands.ts 同步 OnboardOptions、
  OnboardingPlan、OnboardReport 类型与 planOnboarding / onboardProject 封装
- 统筹决定(P8d 审查 ⑩):L1 Codex 列仅因 codex_project_untrusted 未就绪的格不计入 done,
  completion 单列「Codex 信任」(pending_user),next_actions 保留 codex_trust;仓库子目录与
  worktree 补 next_action repo_layout 说明为何到不了 done;非 git 维持不能 done
- 契约同步:project-context.yaml、agent-button-system.md、AGENTS.md 架构速查;P8d 卡记统筹决定落地
- cargo test 413 passed / 0 failed / 6 ignored;pnpm typecheck、pnpm test 通过;三轮 10 处变异全部被杀

Feature-Confirmed: true
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 19:50:21 +09:00

593 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

mod commands;
mod credential_store;
mod db;
mod envoy;
mod mcp_inject;
mod mcp_server;
mod mcp_tools;
mod mcp_webhook;
mod models;
mod onboard;
use git2;
/// Codex 的 http_headers_helper 入口;只被本机同一可执行文件的专用参数调用。
pub fn mcp_client_auth_headers_json(project_digest: &str) -> Result<String, String> {
let token = credential_store::read_mcp_auth_token_for_digest(
&credential_store::WindowsCredentialStore,
project_digest,
)?;
serde_json::to_string(&serde_json::json!({ "Authorization": format!("Bearer {token}") }))
.map_err(|_| "MCP 认证头生成失败".to_string())
}
/// 两段文本忽略行尾差异(CRLF/LF)后是否相同。
pub(crate) fn same_text_ignoring_eol(left: &str, right: &str) -> bool {
left.replace("\r\n", "\n") == right.replace("\r\n", "\n")
}
/// 内容(忽略行尾差异)未变时不重写文件,返回是否真的写入。
/// 子项目常开 core.autocrlf:用 LF 重写 CRLF 检出的同内容文件会改变大小与修改时间,
/// git status 因此误报改动,修改时间也不再能说明「最后是谁改的」(2026-10-01 enterprise 归属梳理实测)。
/// 凡向子项目受跟踪文件写入,都应经此函数(或在其之前做更强的语义比较,如 JSON)。
pub(crate) fn write_if_content_changed(
path: &std::path::Path,
content: &str,
) -> std::io::Result<bool> {
if let Ok(existing) = std::fs::read_to_string(path) {
if same_text_ignoring_eol(&existing, content) {
return Ok(false);
}
}
std::fs::write(path, content).map(|()| true)
}
/// 接入写入汇(agent-client-parity P8c):接入包、蓝图规则、项目身份与 MCP 投影这些项目文件写入
/// 只经它读写。`DiskSink` 真实写入;onboard::plan 的记录汇只记录不落盘,并让后续步骤读到前面
/// 步骤的待写内容(同一文件被两步先后改写时,计划与实际写入算出同样的结果)。
pub(crate) trait WriteSink {
/// 读取当前内容(含本汇里尚未落盘的待写内容);文件不存在返回 Ok(None)。
fn read(&self, path: &std::path::Path) -> std::io::Result<Option<String>>;
/// 内容(忽略行尾)未变返回 Ok(false);否则写入(必要时先建父目录)并返回 Ok(true)。
fn write(&self, path: &std::path::Path, content: &str) -> std::io::Result<bool>;
/// 是否存在(含本汇里的待写内容)。「存在即跳过」的分发只需判存在,不读内容——
/// 子项目自有的非 UTF-8 文件(如 PowerShell 5 重定向生成的 UTF-16 lefthook.yml)照旧跳过,不报读取失败。
fn exists(&self, path: &std::path::Path) -> bool;
}
/// 写入序列中某个文件读写失败:`path` 指向真正出错的文件(计划的阻断项据此给出目标),
/// `reason` 为原有错误文案;转成 String 时只取文案,原有返回 String 的入口行为不变。
#[derive(Debug)]
pub(crate) struct WriteFailure {
pub(crate) path: std::path::PathBuf,
pub(crate) reason: String,
}
impl WriteFailure {
pub(crate) fn at(path: &std::path::Path) -> impl FnOnce(String) -> WriteFailure + '_ {
move |reason| WriteFailure {
path: path.to_path_buf(),
reason,
}
}
}
impl From<WriteFailure> for String {
fn from(failure: WriteFailure) -> String {
failure.reason
}
}
/// 真实写入:内容不变不重写(write_if_content_changed),父目录按需创建。
pub(crate) struct DiskSink;
impl WriteSink for DiskSink {
fn exists(&self, path: &std::path::Path) -> bool {
path.exists()
}
fn read(&self, path: &std::path::Path) -> std::io::Result<Option<String>> {
match std::fs::read_to_string(path) {
Ok(text) => Ok(Some(text)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error),
}
}
fn write(&self, path: &std::path::Path, content: &str) -> std::io::Result<bool> {
if let Some(existing) = self.read(path)? {
if same_text_ignoring_eol(&existing, content) {
return Ok(false);
}
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, content).map(|()| true)
}
}
/// 创建不弹 CMD 窗口的子进程(Windows 专用 CREATE_NO_WINDOW 标志)。
/// 所有后台静默命令都应通过此函数创建,避免在 Windows 上频繁闪出黑框。
pub(crate) fn silent_cmd(program: &str) -> std::process::Command {
let mut cmd = std::process::Command::new(program);
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd
}
use commands::{
activity::*,
blueprint::*,
buff::{apply_blueprint_buff, get_buff_status, list_buffed_projects, remove_blueprint_buff},
canvas::*,
cicd::*,
claude_config::get_beast_global_status,
devtools::*,
git_ops::*,
git_repos::*,
groups::*,
mentor::{
add_project_note, get_group_mentor_report, get_mentor_context, get_project_notes,
get_startup_alerts, run_startup_health_scan,
},
network::{
clear_wsl_proxy, get_wsl_ip, get_wsl_proxy_status, sync_wsl_proxy, test_win_proxy_ip,
test_wsl_proxy_ip,
},
project_scan::*,
project_tools::*,
projects::{
batch_create_projects, create_project, delete_project, get_project, get_projects,
scan_subfolders, update_project, update_project_tags,
},
proxy::*,
publish::*,
settings::{
check_mcp_health, detect_editors, get_agent_autostart_status, get_agent_runtime_status,
get_settings, run_health_check, search_editor_path, set_agent_autostart, update_settings,
},
shell::*,
spaces::*,
tags::*,
templates::*,
toolchain::*,
};
fn has_autostart_arg(args: impl IntoIterator<Item = std::ffi::OsString>) -> bool {
args.into_iter().any(|arg| arg == "--autostart")
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
// 第二个实例启动时,激活已有窗口
use tauri::Manager;
if let Some(w) = app.get_webview_window("main") {
let _ = w.show();
let _ = w.unminimize();
let _ = w.set_focus();
}
}))
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
Some(vec!["--autostart"]),
))
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.setup(|app| {
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager,
};
let data_dir = app.path().app_data_dir().expect("no app data dir");
db::init(data_dir.clone());
commands::blueprint::init_user_conventions_dir(data_dir);
// 禁用 libgit2 目录所有者验证,允许访问 WSL UNC 路径(\\wsl.localhost\...)
unsafe {
let _ = git2::opts::set_verify_owner_validation(false);
}
// 种植内置模板(首次启动时执行)
commands::templates::seed_builtin_templates();
// ── 启动 MCP Server ───────────────────────────────────────
let mcp_port = mcp_server::read_port();
tauri::async_runtime::spawn(mcp_server::start(mcp_port));
// ── 三端共用的稳定 helper 入口与 DSH 托管块(best-effort,失败只记日志)──
// 每次启动把 shim 指向当前 exe,换构建目录后各客户端无需逐项目重注入。
if let Err(error) = mcp_inject::ensure_header_shim() {
db::log_event(
"mcp_inject",
"warn",
&format!("MCP helper 入口写入失败: {error}"),
Some(r#"{"client":"shim"}"#),
);
}
// 刷新 DSH 白名单要读所有项目根目录的 context.json,放到后台线程,不拖慢窗口启动
std::thread::spawn(|| mcp_inject::refresh_dsh_patch(None));
// ── MCP 单一服务器化一次性迁移(settings 标记位防重复)────
std::thread::spawn(|| {
std::thread::sleep(std::time::Duration::from_secs(10)); // 等 DB 就绪
mcp_inject::migrate_to_single_server();
});
// ── Gitea 远端轮询(G6):每 10 分钟拉取绑定仓库 commit ───
// 出站 HTTPS 采集,适配公网 Gitea + 内网炼境拓扑(webhook 不可达时的降级)
std::thread::spawn(|| {
std::thread::sleep(std::time::Duration::from_secs(30)); // 等 DB 与网络就绪
loop {
let _ = commands::gitea::poll_all_repos();
std::thread::sleep(std::time::Duration::from_secs(600));
}
});
// ── 启动健康扫描(异步,不阻塞窗口渲染)────────────────
tauri::async_runtime::spawn(async {
tokio::task::spawn_blocking(run_startup_health_scan)
.await
.ok();
});
// ── 启动蓝图 Buff 后台轮询 ────────────────────────────
commands::buff::start_blueprint_watchers(app.handle().clone());
// ── 系统托盘 ──────────────────────────────────────────────
let show_i = MenuItem::with_id(app, "show", "显示窗口", true, None::<&str>)?;
let quit_i = MenuItem::with_id(app, "quit", "退出炼境", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_i, &quit_i])?;
TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.show_menu_on_left_click(false)
.tooltip("炼境")
.on_menu_event(|app, event| match event.id.as_ref() {
"quit" => app.exit(0),
"show" => {
if let Some(w) = app.get_webview_window("main") {
let _ = w.show();
let _ = w.set_focus();
}
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
// 左键单击托盘图标 → 显示/聚焦窗口
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(w) = app.get_webview_window("main") {
let _ = w.show();
let _ = w.set_focus();
}
}
})
.build(app)?;
// ── 关闭窗口 → 最小化到托盘 ──────────────────────────────
if let Some(window) = app.get_webview_window("main") {
let win = window.clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
let _ = win.hide();
}
});
if has_autostart_arg(std::env::args_os()) {
let _ = window.hide();
}
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
// projects
get_projects,
get_project,
create_project,
update_project,
update_project_tags,
delete_project,
scan_subfolders,
batch_create_projects,
// activity
get_activity,
add_activity,
// groups
get_groups,
create_group,
update_group,
delete_group,
reorder_groups,
get_group_maps,
add_group_member,
remove_group_member,
// settings
get_settings,
update_settings,
detect_editors,
search_editor_path,
check_mcp_health,
get_agent_runtime_status,
get_agent_autostart_status,
set_agent_autostart,
run_health_check,
// project tools
get_project_tools,
add_project_tool,
update_project_tool,
remove_project_tool,
launch_tool,
// global quick launch
get_global_tools,
add_global_tool,
update_global_tool,
remove_global_tool,
// devtools
scan_dev_tools,
scan_wsl_dev_tools,
get_dev_tools,
save_dev_tools,
delete_dev_tool,
upsert_dev_tool,
// wsl proxy
get_wsl_ip,
sync_wsl_proxy,
get_wsl_proxy_status,
clear_wsl_proxy,
test_wsl_proxy_ip,
test_win_proxy_ip,
// v2rayn import
scan_v2rayn_nodes,
detect_v2rayn_db_path,
get_active_v2rayn_node,
// project env scan
scan_project_env,
get_project_env_tools,
save_project_env_tools,
get_toolchain_status,
prepare_toolchain,
// shell
open_editor,
open_terminal,
// git ops
git_clone,
git_status,
git_fetch,
git_pull,
git_push,
git_head_commit,
git_upstream_behind,
git_create_branch,
git_list_branches,
git_checkout_branch,
// spaces
get_spaces,
create_space,
update_space,
delete_space,
set_current_space,
get_deploy_commands,
add_deploy_command,
remove_deploy_command,
execute_deploy,
// git repos registry
get_git_repos,
import_git_repos,
update_git_repo_deploy,
mount_repo,
delete_git_repo,
// 本地 git 工具
scan_local_git,
git_init_and_commit,
git_remote_set,
register_and_link_repo,
write_gitignore,
resolve_wsl_path,
get_wsl_distros,
// project templates
list_templates,
get_template,
save_template,
delete_template,
create_project_from_template,
// tags
get_tags,
create_tag,
update_tag,
delete_tag,
get_project_tags,
set_project_tags,
get_all_project_tag_maps,
// cicd
get_cicd_config,
save_cicd_config,
generate_workflow,
plan_cicd_workflow,
apply_cicd_workflow,
// blueprint
get_blueprint,
generate_blueprint_prompt,
get_blueprint_status,
sync_blueprint_rules,
get_flywheel_stats,
generate_muhe_prompt,
archive_project_docs,
get_user_conventions_status,
save_user_conventions,
reset_conventions_to_builtin,
// canvas state
get_canvas_state,
save_canvas_state,
// blueprint buff
apply_blueprint_buff,
remove_blueprint_buff,
get_buff_status,
list_buffed_projects,
// onboarding pack
commands::onboarding::apply_onboarding_pack,
commands::onboarding::probe_client_parity,
commands::onboarding::plan_onboarding,
commands::onboarding::onboard_project,
// agent infrastructure
commands::agent_infra::scan_agent_infra_stack,
commands::agent_infra::generate_agent_infra_files,
commands::agent_infra::scan_doc_freshness,
commands::agent_infra::get_doc_freshness_cache,
commands::agent_infra::get_agent_health,
commands::commit_metrics::get_commit_stats,
commands::commit_metrics::ingest_full_git_history,
commands::commit_metrics::get_project_commit_health,
commands::commit_metrics::get_all_commit_health,
commands::risk::get_high_risk_scopes,
// mentor
get_mentor_context,
get_project_notes,
add_project_note,
get_group_mentor_report,
get_startup_alerts,
// user todos
commands::user_todos::get_user_todos,
commands::user_todos::add_user_todo,
commands::user_todos::toggle_user_todo,
commands::user_todos::delete_user_todo,
// servers
commands::servers::get_servers,
commands::servers::add_server,
commands::servers::edit_server,
commands::servers::delete_server,
commands::servers::link_server,
commands::servers::unlink_server,
commands::servers::get_project_server_links,
commands::servers::get_server_project_links,
// ssh config & vscode remote
commands::ssh_config::sync_ssh_config,
commands::ssh_config::get_ssh_config_status,
commands::ssh_config::check_vscode_status,
commands::ssh_config::open_vscode_remote,
commands::ssh_config::open_server_terminal,
commands::ssh_config::parse_ssh_config_entries,
commands::ssh_config::import_ssh_config_entries,
// server software & mirrors
commands::server_software::get_github_mirrors,
commands::server_software::add_github_mirror,
commands::server_software::delete_github_mirror,
commands::server_software::test_mirror_local,
commands::server_software::get_server_network_config,
commands::server_software::set_server_mirror,
commands::server_software::get_server_software_list,
commands::server_software::add_server_software,
commands::server_software::update_server_software,
commands::server_software::delete_server_software,
commands::server_software::generate_install_command,
commands::server_software::get_software_categories,
commands::server_software::add_software_category,
commands::server_software::update_software_category,
commands::server_software::delete_software_category,
// api keys
commands::api_keys::get_api_keys,
commands::api_keys::add_api_key,
commands::api_keys::edit_api_key,
commands::api_keys::delete_api_key,
commands::api_keys::reveal_api_key,
// project development variables (human filling; no reveal command)
commands::dev_variables::list_project_dev_variables,
commands::dev_variables::save_project_dev_variable,
commands::dev_variables::save_project_test_account,
commands::dev_variables::reveal_project_dev_variable,
commands::dev_variables::revoke_project_dev_variable,
// host apps
commands::host_apps::get_host_apps,
commands::host_apps::add_host_app,
commands::host_apps::edit_host_app,
commands::host_apps::delete_host_app,
// server service configs
commands::service_config::get_service_configs,
commands::service_config::add_service_config,
commands::service_config::edit_service_config,
commands::service_config::delete_service_config,
// gitea integration
commands::gitea::gitea_save_instance,
commands::gitea::gitea_list_instances,
commands::gitea::gitea_delete_instance,
commands::gitea::gitea_update_instance,
commands::gitea::gitea_test_connection,
commands::gitea::get_project_resource_status,
commands::gitea::gitea_list_linked_repos,
commands::gitea::gitea_get_repo_link,
commands::gitea::gitea_create_repo,
commands::gitea::gitea_migrate_repo,
commands::gitea::gitea_link_repo,
commands::gitea::gitea_setup_webhook,
commands::gitea::gitea_unlink_repo,
commands::gitea::gitea_poll_now,
// beast status (read-only)
get_beast_global_status,
// source library
commands::source_library::get_source_categories,
commands::source_library::create_source_category,
commands::source_library::update_source_category,
commands::source_library::delete_source_category,
commands::source_library::get_source_projects,
commands::source_library::create_source_project,
commands::source_library::update_source_project,
commands::source_library::delete_source_project,
commands::source_library::get_source_versions,
commands::source_library::add_source_version,
commands::source_library::set_primary_version,
commands::source_library::delete_source_version,
commands::source_library::update_version_notes,
commands::source_library::scan_source_dir_tree,
commands::source_library::reveal_source_in_explorer,
commands::source_library::copy_source_to,
// cloud products
commands::cloud_products::get_cloud_products,
commands::cloud_products::add_cloud_product,
commands::cloud_products::edit_cloud_product,
commands::cloud_products::delete_cloud_product,
// cloud db databases
commands::cloud_db_databases::get_cloud_databases,
commands::cloud_db_databases::add_cloud_database,
commands::cloud_db_databases::edit_cloud_database,
commands::cloud_db_databases::delete_cloud_database,
// docker apps
commands::docker_apps::get_docker_apps,
commands::docker_apps::add_docker_app,
commands::docker_apps::edit_docker_app,
commands::docker_apps::delete_docker_app,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[cfg(test)]
mod tests {
#[test]
fn autostart_launch_argument_is_explicit() {
assert!(super::has_autostart_arg([
"dev-manager-tauri.exe".into(),
"--autostart".into(),
]));
assert!(!super::has_autostart_arg([
"dev-manager-tauri.exe".into(),
"--inspect".into(),
]));
}
}