chore: bootstrap nana-story M0

This commit is contained in:
Codex
2026-07-28 12:51:00 +08:00
commit 3dacc92423
71 changed files with 6840 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "nana-story-app"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
description = "Desktop shell for 听娜娜讲故事"
[lib]
name = "nana_story_app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
nana-domain.workspace = true
nana-runtime.workspace = true
serde.workspace = true
serde_json.workspace = true
tauri = { version = "2", features = [] }
[lints]
workspace = true
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build();
}
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capability for the main window",
"windows": ["main"],
"permissions": ["core:default"]
}
+96
View File
@@ -0,0 +1,96 @@
use nana_domain::{
AppInfo, DemoPackSummary, DOMAIN_SCHEMA_VERSION, PlayerView, ResourceBundle, ValidationIssue,
validate_bundle,
};
use serde::Serialize;
const DEMO_PLAYER_VIEW: &str = include_str!("../../fixtures/player-view/initial.json");
const DEMO_BUNDLE: &str = include_str!("../../content/nana-demo/bundle.json");
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandError {
code: &'static str,
message: String,
issues: Vec<ValidationIssue>,
}
impl CommandError {
fn parse(target: &str, error: &serde_json::Error) -> Self {
Self {
code: "invalid_embedded_json",
message: format!("failed to parse embedded {target}: {error}"),
issues: Vec::new(),
}
}
}
#[tauri::command]
fn get_app_info() -> AppInfo {
AppInfo {
name: "听娜娜讲故事".to_owned(),
version: env!("CARGO_PKG_VERSION").to_owned(),
contract_version: DOMAIN_SCHEMA_VERSION,
}
}
#[tauri::command]
fn get_demo_player_view() -> Result<PlayerView, CommandError> {
serde_json::from_str(DEMO_PLAYER_VIEW)
.map_err(|error| CommandError::parse("PlayerView fixture", &error))
}
#[tauri::command]
fn get_demo_pack_summary() -> Result<DemoPackSummary, CommandError> {
let bundle: ResourceBundle = serde_json::from_str(DEMO_BUNDLE)
.map_err(|error| CommandError::parse("content bundle", &error))?;
let report = validate_bundle(&bundle);
if !report.is_valid() {
return Err(CommandError {
code: "invalid_content_bundle",
message: "the embedded content bundle failed domain validation".to_owned(),
issues: report.issues,
});
}
Ok(DemoPackSummary {
id: bundle.id,
display_name: bundle.display_name,
revision: bundle.revision,
characters: bundle.characters.len(),
world_books: bundle.world_books.len(),
personas: bundle.personas.len(),
plot_modules: bundle.plot_modules.len(),
item_specs: bundle.item_specs.len(),
})
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
get_app_info,
get_demo_pack_summary,
get_demo_player_view
])
.run(tauri::generate_context!())
.expect("failed to run nana-story");
}
#[cfg(test)]
mod tests {
use super::{get_demo_pack_summary, get_demo_player_view};
#[test]
fn embedded_player_view_is_valid() {
let view = get_demo_player_view().expect("valid PlayerView fixture");
assert_eq!(view.character_name, "娜娜");
assert!(!view.beats.is_empty());
}
#[test]
fn embedded_content_bundle_is_valid() {
let summary = get_demo_pack_summary().expect("valid demo bundle");
assert_eq!(summary.characters, 1);
assert_eq!(summary.plot_modules, 1);
}
}
+3
View File
@@ -0,0 +1,3 @@
fn main() {
nana_story_app_lib::run();
}
+33
View File
@@ -0,0 +1,33 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "听娜娜讲故事",
"version": "0.1.0",
"identifier": "dev.nanastory.app",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "听娜娜讲故事",
"width": 1440,
"height": 900,
"minWidth": 960,
"minHeight": 640,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": "default-src 'self'; img-src 'self' asset: data:; style-src 'self' 'unsafe-inline'; connect-src 'self' ipc: http://ipc.localhost"
}
},
"bundle": {
"active": true,
"targets": "all",
"shortDescription": "本地优先的 AI 对话游戏"
}
}