Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
//! `nomifun-web` — the standalone Web host for the browser deployment.
|
||||
//!
|
||||
//! In the unified architecture there is ONE Rust backend (`nomifun-app`, the
|
||||
//! former `nomicore`). It runs in two host modes:
|
||||
//! * embedded in the Tauri desktop shell (`apps/desktop`), started in-process
|
||||
//! on a localhost port in `--local` (no-auth) mode — the shell IS the trust
|
||||
//! boundary, so no login is required;
|
||||
//! * here, as a standalone server that ALSO serves the built SPA (`ui/dist`)
|
||||
//! so browsers hit the same HTTP API. This replaces the old Node `web-host`.
|
||||
//!
|
||||
//! Unlike the desktop shell, this host is reachable over the network, so it
|
||||
//! boots the backend in AUTHENTICATED mode by default (login required). On a
|
||||
//! fresh data dir it provisions the first admin out-of-band (see
|
||||
//! `ensure_admin_credentials`), because the in-band setup endpoints are
|
||||
//! local-only. `--insecure-no-auth` opts back into desktop-style no-auth for a
|
||||
//! host that is only reachable over loopback / a trusted private network.
|
||||
//!
|
||||
//! This host boots the backend **in-process** (same binary), composes its `/api`
|
||||
//! router with a static `ServeDir` fallback for the SPA, and serves both on one
|
||||
//! port. Env mutation + runtime init happen before the tokio runtime starts,
|
||||
//! mirroring the `nomicore` bin's ordering.
|
||||
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
/// Env var that, when truthy, opts into `--insecure-no-auth` without the flag.
|
||||
const ENV_INSECURE_NO_AUTH: &str = "NOMIFUN_WEB_INSECURE_NO_AUTH";
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "nomifun-web",
|
||||
about = "NomiFun unified Web host (SPA + backend API)"
|
||||
)]
|
||||
struct Args {
|
||||
/// Host/IP address to bind on. Defaults to loopback; use `0.0.0.0` to accept
|
||||
/// connections from other machines (do so behind a trusted gateway / private
|
||||
/// network — see `--insecure-no-auth`).
|
||||
#[arg(long, env = "NOMIFUN_WEB_HOST", default_value = "127.0.0.1")]
|
||||
host: String,
|
||||
/// Port to listen on (serves both the API and the SPA).
|
||||
#[arg(long, env = "NOMIFUN_WEB_PORT", default_value_t = 8787)]
|
||||
port: u16,
|
||||
/// Data directory for the backend (db + storage). Defaults to the same
|
||||
/// per-user dir as the desktop shell (`%LOCALAPPDATA%\NomiFun\Nomi` on
|
||||
/// Windows, see `nomifun_app::cli::default_data_dir`) so every host and
|
||||
/// dev loop shares one state by default. The env value is taken literally
|
||||
/// (no `/Nomi` suffix) — production deployments (Docker `/data`, systemd
|
||||
/// `/var/lib/nomifun`) rely on that.
|
||||
#[arg(
|
||||
long,
|
||||
env = "NOMIFUN_DATA_DIR",
|
||||
default_value_os_t = nomifun_app::cli::default_data_dir(),
|
||||
value_parser = nomifun_app::cli::parse_non_empty_path
|
||||
)]
|
||||
data_dir: PathBuf,
|
||||
/// Directory containing the built SPA (ui/dist).
|
||||
#[arg(long, env = "NOMIFUN_WEB_DIST", default_value = "../../ui/dist")]
|
||||
dist: PathBuf,
|
||||
/// DANGER: run the backend in local mode — authentication is fully DISABLED
|
||||
/// and every client acts as a privileged user with shell/file/agent access.
|
||||
/// Only for a host reachable solely over loopback or a trusted private
|
||||
/// network. Without this flag the web host requires a login (safe default).
|
||||
/// Can also be enabled via `NOMIFUN_WEB_INSECURE_NO_AUTH=true`.
|
||||
#[arg(long)]
|
||||
insecure_no_auth: bool,
|
||||
/// Initial admin username provisioned on first run (authenticated mode only).
|
||||
/// Ignored once an admin exists.
|
||||
#[arg(long, env = "NOMIFUN_ADMIN_USERNAME", default_value = "admin")]
|
||||
admin_user: String,
|
||||
/// Initial admin password provisioned on first run (authenticated mode only).
|
||||
/// If omitted, no admin is pre-seeded: the install is left uninitialised and
|
||||
/// the first WebUI visitor creates the admin interactively via first-run
|
||||
/// setup (`POST /api/auth/setup`). Ignored once an admin exists.
|
||||
#[arg(long, env = "NOMIFUN_ADMIN_PASSWORD")]
|
||||
admin_password: Option<String>,
|
||||
}
|
||||
|
||||
/// Parse a truthy env value (`1`/`true`/`yes`/`on`, case-insensitive).
|
||||
fn env_flag(name: &str) -> bool {
|
||||
std::env::var(name)
|
||||
.map(|v| {
|
||||
matches!(
|
||||
v.trim().to_ascii_lowercase().as_str(),
|
||||
"1" | "true" | "yes" | "on"
|
||||
)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn main() -> Result<ExitCode> {
|
||||
// If an ACP agent CLI spawned this binary as an MCP stdio bridge
|
||||
// (`current_exe() mcp-requirement-stdio` etc.), run that helper and exit
|
||||
// BEFORE clap parses our own Args (which would reject the subcommand) and
|
||||
// before any backend/server init. Every host binary must honor these or the
|
||||
// injected declaration tools (requirement_complete / team / guide) never
|
||||
// appear in the agent's session.
|
||||
if let Some(code) = nomifun_app::commands::run_mcp_stdio_subcommand_if_present() {
|
||||
return Ok(code);
|
||||
}
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
// Authentication is ON by default; `--insecure-no-auth` (or the env var)
|
||||
// opts into the desktop-style no-auth local mode. The env is read manually
|
||||
// to avoid clap's bool+env flag ambiguity.
|
||||
let insecure_no_auth = args.insecure_no_auth || env_flag(ENV_INSECURE_NO_AUTH);
|
||||
|
||||
// Build a fully-defaulted backend CLI without touching this process's argv,
|
||||
// then override the bits this host owns. `parse_from` gives a defaulted Cli.
|
||||
let mut cli = nomifun_app::cli::Cli::parse_from(["nomifun-web"]);
|
||||
cli.host = args.host.clone();
|
||||
cli.port = args.port;
|
||||
cli.data_dir = args.data_dir.clone();
|
||||
cli.local = insecure_no_auth;
|
||||
|
||||
// Same ordering as the nomicore bin: runtime init + PATH enhancement BEFORE
|
||||
// any worker thread / tokio runtime exists.
|
||||
nomifun_runtime::init(&cli.data_dir);
|
||||
// SAFETY: called before the tokio runtime (and its threads) is built.
|
||||
let merged_path = unsafe { nomifun_runtime::enhance_process_path() };
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
runtime.block_on(serve(cli, merged_path, args))
|
||||
}
|
||||
|
||||
async fn serve(cli: nomifun_app::cli::Cli, merged_path: String, args: Args) -> Result<ExitCode> {
|
||||
// Resolve the bind address up front so a bad --host fails fast with a clear
|
||||
// message instead of a cryptic socket error.
|
||||
let ip: IpAddr = args.host.parse().with_context(|| {
|
||||
format!(
|
||||
"invalid --host '{}': expected an IP like 127.0.0.1 or 0.0.0.0",
|
||||
args.host
|
||||
)
|
||||
})?;
|
||||
if !ip.is_loopback() && cli.local {
|
||||
tracing::warn!(
|
||||
%ip,
|
||||
"binding a non-loopback address with --insecure-no-auth: authentication is DISABLED — \
|
||||
anyone who can reach this port gets full host access (shell, files, agents). \
|
||||
Put a trusted gateway in front, use a private network, or drop --insecure-no-auth."
|
||||
);
|
||||
}
|
||||
|
||||
// Boot the backend in-process (env → data layer → services), then mount the
|
||||
// real API router with the SPA as the fallback for non-/api routes.
|
||||
let env = nomifun_app::bootstrap::init_environment(&cli, &merged_path)?;
|
||||
let database = nomifun_app::bootstrap::init_data_layer(&env.config).await?;
|
||||
let services = nomifun_app::AppServices::from_config(database, &env.config).await?;
|
||||
|
||||
// First-run admin provisioning. No-op in local mode and once an admin
|
||||
// exists; otherwise a fresh authenticated install would have no way to set
|
||||
// the first password (the in-band setup routes are local-only). Returns
|
||||
// whether the install still awaits interactive first-run setup.
|
||||
let needs_first_run_setup = nomifun_app::bootstrap::ensure_admin_credentials(
|
||||
&services,
|
||||
nomifun_app::bootstrap::AdminBootstrap {
|
||||
username: Some(args.admin_user.clone()),
|
||||
password: args.admin_password.clone(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if needs_first_run_setup && !ip.is_loopback() {
|
||||
tracing::warn!(
|
||||
%ip,
|
||||
"first-run setup is OPEN on a non-loopback address: the NEXT client to reach this \
|
||||
port will create the admin account. Complete setup over a trusted network/tunnel \
|
||||
first, or pre-seed with NOMIFUN_ADMIN_PASSWORD."
|
||||
);
|
||||
}
|
||||
|
||||
let api = nomifun_app::create_router(&services).await;
|
||||
let app = api
|
||||
.fallback_service(
|
||||
ServeDir::new(&args.dist)
|
||||
.append_index_html_on_directories(true)
|
||||
.fallback(ServeFile::new(args.dist.join("index.html"))),
|
||||
)
|
||||
.layer(TraceLayer::new_for_http());
|
||||
|
||||
let addr = SocketAddr::new(ip, args.port);
|
||||
tracing::info!(
|
||||
requested = %addr,
|
||||
auth = if cli.local { "disabled (insecure-no-auth)" } else { "required" },
|
||||
dist = ?args.dist,
|
||||
"nomifun-web: embedded backend + SPA on one port"
|
||||
);
|
||||
// Port failover: if `args.port` is taken, bind a bounded-scan neighbour (or
|
||||
// an ephemeral port) instead of hard-failing, then announce the actually
|
||||
// bound port via `{data_dir}/port.json` + stdout so the operator/launcher
|
||||
// can re-point clients — a browser cannot self-discover a moved port.
|
||||
let (actual_port, listener) = nomifun_app::bootstrap::bind_with_fallback(ip, args.port).await?;
|
||||
if actual_port != args.port {
|
||||
tracing::warn!(
|
||||
requested = args.port,
|
||||
actual = actual_port,
|
||||
"preferred port was busy; bound a fallback port"
|
||||
);
|
||||
}
|
||||
nomifun_app::bootstrap::announce_bound_port(&cli.data_dir, &args.host, actual_port);
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
services.database.close().await;
|
||||
drop(env);
|
||||
Ok(ExitCode::SUCCESS)
|
||||
}
|
||||
Reference in New Issue
Block a user