koharu-app 深入解析:Koharu 的 Tauri 应用状态、命令 API 与权威前端契约

【免费下载链接】koharu

ML-powered manga translator, written in Rust.

项目地址: https://gitcode.com/gh_mirrors/ko/koharu
点击查看 免费下载

koharu-app 是 Koharu(Rust 编写的 ML 漫画翻译工具)桌面端的核心应用层 crate:它全权持有 Tauri 管理的应用状态、命令 API、项目生命周期、处理作业、类型化事件通道与 Agent host,并借助 koharu-desktop 为浏览器画布准备与发布页面帧。读完本文,你将掌握 koharu-app 的模块边界、49 条 Tauri 命令的组成与用途、cargo run -p koharu-app --bin generate 契约生成链路,以及类型化通道、项目库存储、pipeline 作业等关键机制在源码中的具体实现。

koharu-app 的定位:把「应用」从「引擎」中分离

在 Koharu 的工作区中,koharu-pipeline、koharu-scene、koharu-renderer、koharu-rasterizer、koharu-translator、koharu-ml 等 crate 构成了无界面的能力引擎;而 crates/koharu-app/README.md 明确指出,koharu-app 是这些引擎之上的「Tauri 应用壳」:

  • 拥有 Tauri-managed application state(通过 application.manage(...) 注册的所有状态对象);
  • 暴露 command API(全部 Tauri IPC 命令);
  • 驱动 project lifecycle(创建、打开、关闭、删除、导入、选页);
  • 调度 processing jobs(pipeline 运行、停止、进度广播);
  • 维护 typed channels(画布、作业、下载、资源、项目五种实时事件通道);
  • 托管 agent host(登录、运行、取消 agent 运行);
  • 使用 koharu-desktop 把页面帧准备并发布给浏览器画布。

koharu-app 只向外暴露两个符号(见 crates/koharu-app/src/lib.rs):

pub use app::run;
pub use commands::bindings;

run(context) 启动整个桌面运行时,bindings() 返回 tauri_specta::Builder,它同时是 IPC invoke handler 与 TypeScript 契约的唯一来源——这正是 README 强调的 "Rust command signatures are the authoritative frontend contract"(Rust 命令签名是权威前端契约)的工程落地。

依赖层面(见 crates/koharu-app/Cargo.toml),koharu-app 几乎聚合了全部业务 crate:koharu-agent、koharu-desktop、koharu-ml、koharu-pipeline、koharu-psd、koharu-renderer、koharu-rasterizer(启用 native feature)、koharu-runtime、koharu-scene、koharu-translator,并直接使用 tauri、tauri-runtime-cef、tauri-specta、specta、specta-typescript 构建前端契约。

命令 API 与契约生成链路

生成命令:cargo run -p koharu-app --bin generate

README 给出了唯一的构建期命令:

cargo run -p koharu-app --bin generate

该二进制位于 crates/koharu-app/src/bin/generate.rs,逻辑非常精简:取 koharu_app::bindings(),开启 semantic_types(Configuration::empty().enable_lossless_floats())(保证 f32/f64 浮点字段在 TypeScript 侧按无损语义映射),然后通过 specta-typescript 把全部命令签名导出到工作区目录下的 packages/bridge/src/protocol.ts,文件头写入 // generated by koharu; do not edit:

let output =
    std::path::Path::new(env!("CARGO_WORKSPACE_DIR")).join("packages/bridge/src/protocol.ts");
koharu_app::bindings()
    .semantic_types(Configuration::empty().enable_lossless_floats())
    .export(
        Typescript::default().header("// generated by koharu; do not edit\n"),
        &output,
    )

生成产物 packages/bridge/src/protocol.ts 以 import { invoke as __TAURI_INVOKE, Channel } from "@tauri-apps/api/core" 开头,为每条命令生成带完整参数类型与返回类型的封装函数(如 getAgentStatus、loginAgent、createProject、process 等),前端直接 import { commands } from "@bridge/protocol" 即可类型安全地调用。改 Rust 命令签名后必须重新运行 generate,前端契约才会同步,这是本 crate 最重要的开发约定。

命令注册与错误模型

bindings() 的实现见 crates/koharu-app/src/commands/mod.rs:

Builder::new()
    .commands(collect_commands![
        agent::get_agent_status,
        agent::login_agent,
        // ... 共 49 条
    ])
    .disable_serde_phases()
    .error_handling(ErrorHandlingMode::Throw)
  • ErrorHandlingMode::Throw:命令返回的错误直接以异常形式抛给前端调用方;
  • 自定义错误类型 Error 用 #[specta(transparent)] 注解为 String,并实现 Serialize 把 anyhow::Error 序列化为 format!("{:#}") 的多行可读文本——前端拿到的永远是带上下文的错误字符串,而不是晦涩的内部结构;
  • 任一 anyhow::Error 可通过 From 自动转换为 Error,因此命令内部可以自由使用 ? 传播错误。

全部 49 条命令按模块划分如下(与 collect_commands! 列表一一对应):

模块命令职责
agentget_agent_status / login_agent / logout_agent / save_agent_config / run_agent / cancel_agentAgent host:登录态、配置、运行与取消
lifecyclesubscribe / get_project / get_pages / get_page / list_projects / create_project / open_project / delete_project / close_project / import / select_page启动订阅与项目生命周期
editingrename_page / delete_pages / move_page / set_source_text / set_translation / set_typography / set_geometry / set_visibility / delete_layers / move_layer / undo / redo文档编辑与撤销重做
processingprocess / stop_job启动与停止 pipeline 作业
outputexport / get_thumbnailPNG/PSD/CBZ 导出与缩略图
fontsget_fonts / get_font_preview字体枚举与预览
preferencessave_preferences / get_preferences / get_translation_models偏好设置与翻译模型列表
canvasget_canvas_manifest / get_canvas_resource / prepare_canvas_page / get_canvas_page_manifest / get_canvas_page_resource / add_point_text / add_text_box / commit_paint / commit_erase / commit_transform / commit_inpaint画布帧、资源与编辑提交

应用启动与状态管理(app.rs)

app::run(见 crates/koharu-app/src/app.rs)是桌面端唯一的入口函数,它完成三件事:配置 CEF runtime、注册插件、在 setup 中初始化状态与后台任务。

CEF runtime 与平台差异配置

let cef = Cef::default();
#[cfg(debug_assertions)]
let cef = cef.remote_debugging(tauri_runtime_cef::RemoteDebugging::Port {
    port: 4000,
    allowed_origins: Vec::new(),
});
#[cfg(target_os = "linux")]
let cef = cef
    .enable_features(["Vulkan", "VulkanFromANGLE"])
    .command_line_args([
        ("--enable-unsafe-webgpu", None),
        ("use-angle", Some("vulkan")),
        ("--ozone-platform", Some("x11")),
    ]);
  • 调试构建(debug_assertions)会开启 CEF 远程调试端口 4000,便于前端调试;
  • Linux 下显式启用 Vulkan / WebGPU 特性(--enable-unsafe-webgpu、use-angle=vulkan、--ozone-platform=x11),这是因为 koharu-canvas 在浏览器侧依赖 WebGPU 做高性能合成;
  • 平台专属配置还散落在 tauri.linux.conf.json、tauri.macos.conf.json、tauri.windows.conf.json 与 Entitlements.plist(macOS 签名权限)中,由 tauri.conf.json 按平台合并。

插件栈

Builder::<CefRuntime>::new() 注册了六个官方插件:

  • tauri-plugin-log:日志写入 LogDir,单文件上限 1,000,000 字节,等级 Info;
  • tauri-plugin-single-instance:单实例约束,二次启动时聚焦已存在的 main 窗口(unminimize + show + set_focus);
  • tauri-plugin-window-state:持久化窗口的 SIZE、POSITION、MAXIMIZED、FULLSCREEN 状态;
  • tauri-plugin-opener / tauri-plugin-process:外部打开与进程管理;
  • tauri-plugin-updater:应用自更新。

invoke_handler(crate::commands::bindings().invoke_handler()) 把前面生成的 tauri_specta::Builder 直接挂载为 IPC 分发器——命令注册与契约生成共享同一份命令清单,从机制上杜绝了「契约与实现不同步」。

setup:托管状态一览

setup 闭包中通过 application.manage(...) 注册了全部共享状态:

application.manage(CurrentProject { project: Mutex::new(None) });
application.manage(ProjectLibrary::new()?);
application.manage(Processing::default());
application.manage(CanvasChannel::default());
application.manage(JobChannel::default());
application.manage(DownloadChannel::default());
application.manage(ResourceChannel::default());
application.manage(ProjectChannel::default());
application.manage(Initialization::default());
application.manage(koharu_desktop::Desktop::new()?);
application.manage(AgentState::new(handle.clone())?);

随后从 tauri.conf.json 的 app.windows 配置中查找 label == "main" 的窗口定义,用 WebviewWindowBuilder::from_config 构建并显示主窗口。

Windows 发布构建(cfg(all(target_os = "windows", not(debug_assertions))))还会调用 koharu_runtime::Store::configure(...),把模型运行时仓库定位到安装目录下的 store 子目录。

异步初始化:initialize

窗口创建后立即 spawn 一个后台任务执行 initialize(crates/koharu-app/src/app.rs 第 23–66 行),其顺序是:

  1. koharu_ml::init().await 初始化 ML 运行时;
  2. koharu_ml::device(false) 探测计算设备,并把 compute_backend、device_type、gpu_model、vram_bytes 写入 koharu_metrics::context(该字段注入 app_started 事件,形成首次启动的硬件画像);
  3. koharu_pipeline::Pipeline::load(device)? 加载处理管线并 handle.manage;
  4. 订阅 pipeline.subscribe_resources(),循环把 ResourceSnapshot 转成 ModelResources 推送到 ResourceChannel;
  5. 若已有打开的项目,调用 desktop.show_page(&snapshot, page) 恢复当前页,否则 desktop.clear();
  6. 初始化完成后 Initialization::ready() 置位(tokio::sync::watch 通道),唤醒所有等待 subscribe 的调用方。

下载事件桥接

koharu_runtime::download::subscribe() 的广播事件在 setup 的另一个后台任务中被转为 Download 结构体(含 id、state、name、completed、total、error)推入 DownloadChannel;Started/Progress/Finished/Failed 四类事件同时以 download_start、download_progress、download_result 指标写入 koharu_metrics。

窗口关闭的清理协议

on_window_event 中,窗口 CloseRequested 或 Destroyed 时:

  • 遍历 Processing.stops 调用 stop.stop() 停止所有 pipeline 作业并清空;
  • 调用 AgentState::cancel_all() 取消 agent 运行;
  • Destroyed 时上报 app_closed 指标并 koharu_metrics::shutdown() 收尾。

类型化通道与实时事件推送

通道抽象

commands/mod.rs 定义了 ChannelExt trait,为 Mutex<Option<Channel<T>>> 提供统一的 publish 语义:

impl<T: IpcResponse> ChannelExt<T> for Mutex<Option<Channel<T>>> {
    fn publish(&self, value: T) {
        let mut channel = self.lock();
        if channel
            .as_ref()
            .is_some_and(|channel| channel.send(value).is_err())
        {
            channel.take();
        }
    }
}

即:有订阅者就推,推送失败(前端已释放)则丢弃通道。五个通道 CanvasChannel、JobChannel、DownloadChannel、ResourceChannel、ProjectChannel 全部复用此模式,共享状态中只保存「最新一个」前端通道。

subscribe:前端的第一条命令

lifecycle::subscribe(crates/koharu-app/src/commands/lifecycle.rs)是前端的启动握手:它接收五个 Channel 参数(on_canvas、on_job、on_download、on_resources、on_project),先 Initialization.wait() 等待后台初始化完成,再依次注册五个通道,最后返回 StartupState { preferences, jobs, canvas } 快照——偏好、进行中的作业、当前画布状态一次到位,避免首屏闪烁。

事件负载结构

  • CanvasState:page、revision、generation、size、element_frames(可选中元素的变换帧列表),由 koharu-desktop 生成;
  • Job:id(JobId,Uuid v4 的透明包装)、state(Running/Finished/Failed/Stopped)、completed/total、page、stage、model、error;
  • Download / ModelResources:见上文下载桥接与资源快照;ModelResources 包含 process_memory、system_memory、process_cpu 及每个设备的 DeviceResources(name、selected、memory_budget、memory_used、utilization)。

大整数字段(u64/usize)普遍使用 #[specta(type = f64)] 映射,规避 JS 端 Number 精度上限的边界问题。

项目生命周期与文档存储

项目库:Documents/Koharu

ProjectLibrary(crates/koharu-app/src/commands/project.rs)把项目根目录固定在系统「文档」目录下的 Koharu 文件夹:

let root = dirs::document_dir()
    .context("the Documents directory is unavailable")?
    .join("Koharu");

list() 扫描其中满足以下条件的子目录作为项目:目录名以 .khrproj 结尾,且内部存在 state-a.khr 或 state-b.khr 文件(koharu-storage 的双槽快照文件);按 state-a/b.khr 最近修改时间降序、名称升序排序,作为「最近使用」列表返回 ProjectSummary。

项目会话与撤销/重做

Project 持有 koharu_scene::Session、name、active_page 与两套 revision 栈(undo: Vec<Vec<Revision>>、redo):

  • create/open 通过 Session::create/Session::open 建立会话,新项目自动激活第一页;
  • 所有编辑最终都收敛到 commit(patch) → session.commit(patch),成功后 record(revisions) 压入 undo 栈并清空 redo 栈;
  • undo/redo 通过 session.undo_many(revisions) 批量回滚多个 revision,失败时回滚自身栈以保持一致性;
  • ProjectInfo 暴露 revision、active_page、can_undo、can_redo,前端据此渲染撤销/重做按钮状态。

生命周期命令的公共流程

create_project、open_project 都走 replace_project:

  1. AgentState::reset() 重置 agent,停止并清空所有 pipeline 作业;
  2. 原子替换 CurrentProject;
  3. desktop.show_page(&snapshot, page) 重建画布帧;
  4. 通过 CanvasChannel 与 ProjectChannel 广播新的画布状态与 ProjectInfo。

close_project / delete_project 则走 close_current_project(停止任务 → desktop.clear() → 广播空状态)。delete_project 若删除的是当前项目会先关闭,再在 spawn_blocking 中执行 remove_dir_all,避免阻塞主线程。

导入:Files / Folder

import 命令(crates/koharu-app/src/commands/lifecycle.rs)先用 rfd::AsyncFileDialog 弹出系统文件选择器:

  • PageImportSource::Files:多选文件;
  • PageImportSource::Folder:选择目录后用 walkdir 递归收集,并按 import::Format 解析扩展名过滤。

随后 tokio_rayon::spawn 调用 import::import(files)(crates/koharu-app/src/commands/import/mod.rs)在 Rayon 线程池中并行解码:Raster(png/jpg/jpeg/webp)直接读文件,Zip/Rar 解压,Pdf 渲染成页;路径按 alphanumeric_sort 自然排序保证页序正确。导入期间若 processing.stops 非空则直接拒绝("pages cannot be imported while processing is running")。

导入成功后以 AssetRole::new("source") 把每页原始位图作为资产写入场景快照,提交 revision 并 desktop.synchronize 同步画布。

处理作业与 pipeline 集成

process 命令(crates/koharu-app/src/commands/processing.rs)是引擎与 UI 之间的桥梁,流程如下:

  1. 取当前项目 snapshot,生成 JobId(Uuid v4);
  2. 单任务互斥:Processing.stops 非空时返回 another process is already running;
  3. 登记 StopToken 与初始 Job { state: Running, ... },立即通过 JobChannel 广播;
  4. 克隆 Pipeline,spawn 异步任务执行 pipeline.execute(snapshot, request, &mut committer);
  5. request.progress 回调把 Progress 事件折算为 completed/total(pages.len() * stages.len() 为总量),逐条更新 Processing.jobs 并推送 Job,同时以 pipeline_start、stage_loading、model_run、stage_skip 等指标上报;
  6. 运行结束按 RunStatus::Stopped / 错误 / 成功三种结局置 JobState::Stopped / Failed / Finished,移除 stop 登记并广播终态。

PipelineCommitter 实现 koharu_pipeline::Committer,把每个阶段的 StageOutput.patch 交给 project.commit_rebased(patch)——先用当前快照做 rebase(koharu_scene::Patch::rebase_on),若 pipeline 输出已被用户编辑覆盖(PatchConflict/EntityNotFound/RelationNotFound)则安全放弃该阶段输出,避免文档被陈旧结果污染;提交成功后 desktop.synchronize 并广播新画布。

stop_job 通过 JobId 查找 StopToken 并调用 stop(),协作式取消会沿 pipeline 逐级传播。

画布:koharu-desktop 的帧发布

koharu-app 不直接渲染画布,而是把这项工作委托给 crates/koharu-desktop:它保留当前 koharu-renderer 帧、向前端发布帧 generation、按精确 generation 提供 manifest 与独立寻址的资源、校验可持久化的变换提交、并惰性持有导出与预览共用的原生 koharu-rasterizer。

帧与资源的分发协议

CanvasState 里的 generation 是单调递增的帧代次(wrapping_add(1).max(1),首个有效代次从 1 开始)。前端拿到 CanvasState 后:

  • get_canvas_manifest(generation) → desktop.frame_manifest_bytes(generation),返回 prepared().manifest().encode() 的字节;
  • get_canvas_resource(generation, resource) → 按 ResourceId 返回 resource_packet(resource).encode();
  • 若 generation 已过期,frame_at_generation 直接报错 "canvas generation X is stale",前端据此重新订阅。

字节响应通过自定义 CanvasBytes 类型实现 tauri::ipc::IpcResponse(body() 返回 InvokeResponseBody),在 #[specta(transparent)] 下映射为 Vec<u8>——大体积二进制不经 JSON 序列化,直接走 IPC 原始字节通道。页面级(prepare_canvas_page / get_canvas_page_manifest / get_canvas_page_resource)则按 (page, revision) 寻址,允许前端为后续页面预取。

页面帧缓存与 latest-wins 语义

Desktop 内部维护 PageFrameCache(LRU,上限 MAX_CACHED_PAGE_FRAMES = 8 页,见 crates/koharu-desktop/src/lib.rs),prepare_page 命中缓存直接返回。所有渲染入口都遵循 latest-wins 协议:request_preparation() 使序号自增并通知等待者,过期的准备请求(is_current_preparation == false)在渲染完成后既不落盘也不替换当前帧——配套的两个单元测试(newer_request_preempts_current_preparation_and_acquires_ownership、obsolete_request_cannot_replace_the_presentation)直接验证了这一语义。

transform_geometries 是变换提交的守门员:校验 revision 一致、元素不重复、帧有限且非空、目标层可见且为文本层,然后把控制帧的位移/缩放/旋转反算成场景几何点(仿射系数见 frame_transform),返回给 commit_transform 持久化。

画笔与局部重绘

  • commit_paint / commit_erase:前端把笔画点列与画笔(直径 + RGBA 颜色)提交给 apply_raster_stroke,在 Rust 侧光栅化为 PNG 资产写回场景(RasterStrokeMode::Paint / Erase,橡皮擦强制要求已存在的栅格层);
  • commit_inpaint:前端笔画被 encode_mask 光栅化成单通道灰度遮罩 PNG,连同 Bounds 存入 Processing.inpainting_mask,然后以 Scope::Region { page, bounds } + Operation::Only { stage: Inpainting } 启动一次仅含修复阶段的 pipeline 作业——局部涂鸦 → 遮罩编码 → 定向推理整条链路都在命令层完成;
  • 所有编辑命令都带 expected_revision,ensure_revision 不匹配即拒绝("canvas edit expected revision X, but the project is at Y"),从 IPC 层面防止并发编辑下的乐观锁失效。

编辑、偏好与导出命令

编辑命令族(editing.rs)

rename_page / delete_pages / move_page / set_source_text / set_translation / set_typography / set_geometry / set_visibility / delete_layers / move_layer / undo / redo 覆盖了文档树的全部可变操作,统一模式为:snapshot.patch(...) 构造补丁 → project.commit(patch) → record_commit → desktop.synchronize → 广播 CanvasState。

值得注意的语义细节:

  • set_source_text / set_translation 会先 promote_entity_to_user 把引擎自动生成的层提升为用户编辑层(Origin::User),保证后续用户修改不会被 pipeline 输出覆盖;
  • set_translation(None) 表示移除译文组件;
  • set_geometry 只允许文本层修改几何,且 points == None 时仅当该层是自动放置文本(automatic_target().is_some())才允许重置;
  • delete_layers 遇到文本组会先展开子层,删除后把失去唯一 Presents 引用的孤儿文本内容一并清理,且拒绝删除页面("delete pages with delete_pages")。

偏好设置(preferences.rs)

get_preferences / save_preferences / get_translation_models 管理全局偏好,StartupState 首帧即携带完整 Preferences(生成参数 temperature、top_p、min_p、repeat_penalty、frequency_penalty、presence_penalty,处理器阈值如 koharu-layout-rfdetr-seg-2xl 的 text_threshold、bubble_threshold、panel_threshold 等均可从 packages/bridge/src/protocol.ts 的生成类型中看到)。偏好写入使用 koharu-secrets 加密存储敏感项。

导出(output.rs)

export 命令支持三种格式(crates/koharu-app/src/commands/output.rs):

  • Png / Psd:弹出目录选择器,Psd 走 koharu_psd::export_page 保留图层结构;Png 用 Rasterizer::rasterize 把每个渲染帧合成为位图;
  • Cbz:保存文件对话框(默认文件名 <project>.cbz),逐页栅格化后打包 zip。

所有页面帧用 try_join_all 并行渲染,PNG 编码在 Rayon 中并行执行(PngEncoder::new_with_quality(CompressionType::Best, FilterType::Adaptive))。get_thumbnail 以 THUMBNAIL_EDGE = 128 为边长输出缩略图字节。

前端契约的工程实践总结

koharu-app 的架构可以用一句话概括:Rust 是唯一的真相源(single source of truth)。所有命令签名、类型、错误与事件负载都定义在 Rust 侧,经 tauri-specta 自动推导出 TypeScript 契约;cargo run -p koharu-app --bin generate 一次性完成「注册 invoke handler」与「生成 protocol.ts」两份产物,前端 packages/bridge 只依赖这份生成文件,不存在手写接口漂移的空间。

如果你要在这个仓库上做二次开发或调试,以下路径值得优先阅读:

【免费下载链接】koharu

ML-powered manga translator, written in Rust.

项目地址: https://gitcode.com/gh_mirrors/ko/koharu
点击查看 免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值