dev-manager-tauri/src-tauri/src/lib.rs
lanrtop 019d81c60e
All checks were successful
Push & PR Check / check (pull_request) Successful in 1m3s
fix(onboarding): 审查修复——JSON 按语义比较,CI workflow 与生成文件同内容不重写
对抗审查(9 个 agent,2 条成立):
- settings.json / context.json 改为按 JSON 语义比较:Claude Code 改写后键序与末尾换行不同,
  按文本比较仍会让每次注入整文件重排
- plan_cicd_workflow 忽略行尾判 noop:原按字节比较,autocrlf 检出的同内容 workflow 会被判 update,
  并请用户确认一次无变化的写入;apply 与 generate_workflow 只在内容变化时落盘
- 同类遗漏统一改用 write_if_content_changed:agent_infra 生成文件(未变时报 skipped)、notes-archive 回填
- lib.rs 抽出 same_text_ignoring_eol,与 write_if_content_changed 共用同一比较规则

验证:cargo test 245 passed / 5 ignored(新增 JSON 语义与 CI CRLF 回归断言)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:51:33 +09:00

523 lines
21 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 mcp_inject;
mod mcp_server;
mod mcp_tools;
mod mcp_webhook;
mod models;
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)
}
/// 创建不弹 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,
// 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(),
]));
}
}