架构与技术栈¶
本文描述 custom-base 的 React / Python / Electron 应用。main 用于同步上游,
flutter 独立维护 Flutter / Rust 版本;本页不描述这两个分支的实现。
开发命令见本地开发,分支规则见仓库的
AGENTS.md。
技术栈¶
| 职责 | 技术与用途 |
|---|---|
| 前端界面与构建 | React、TypeScript、Vite;Tailwind CSS 与项目样式,Motion 动效、Lucide 图标 |
| 客户端业务状态 | Zustand;按正文、设置、预览、偏好和通知划分 store |
| 服务端数据 | TanStack Query;原生 Fetch 请求,Valibot 校验响应与保存格式 |
| 数学与文本转换 | KaTeX 解析及项目转换工具;Markdown 清洗、书信排版、插图占位符处理 |
| 本地保存 | Web 使用 localStorage + IndexedDB;桌面通过受限 Electron IPC 读写应用数据目录 |
| 生成服务 | Python、FastAPI;Uvicorn 开发服务,Gunicorn / Uvicorn Worker 生产服务 |
| 排版与导出 | handrightbeta、Pillow、项目数学字形与布局模块;OpenCV 检测背景,PyMuPDF 导出 PDF |
| 后端任务 | SQLite 持久化队列;WebSocket 推送进度,HTTP 轮询作为回退 |
| 桌面与离线入口 | Electron + PyInstaller;Web 使用项目自有 TypeScript Service Worker 缓存应用外壳 |
| 验证与工程工具 | Playwright、Node 测试、pytest;Oxlint / Oxfmt、Ruff / Pyright、Lefthook、semantic-release |
具体版本以 frontend/package.json、desktop/package.json、pyproject.toml 和锁文件为准。
界面统一使用霞鹜文楷;纸张内的字形由用户选择的手写字体与后端缺字回退逻辑决定。
目录与职责¶
主界面为单工作台,使用帮助、桌面下载等入口以弹窗展示。业务模块按 features/ 组织。
| 位置 | 职责 |
|---|---|
frontend/src/main.tsx |
先恢复本地工作区,再安装保存订阅、Query Provider 与业务操作入口 |
frontend/src/App.tsx、components/ |
界面组合、表单与交互;组件按需订阅状态 |
frontend/src/shared/ |
共用 UI、图标与动效基础 |
frontend/src/features/*/store.ts |
各业务域的内存状态与同步状态变更 |
frontend/src/features/workspace/ |
导入、设置、生成等跨域操作编排,以及工作区恢复与保存订阅 |
frontend/src/features/settings/ |
参数解析、旧格式转换和预设仓库 |
frontend/src/features/generation/ |
生成操作、任务会话、Query 缓存订阅与重试倒计时 |
frontend/src/api/ |
请求、响应校验、Query 配置、任务接口与下载适配 |
frontend/src/persistence/ |
记录协议、Web / 桌面存储适配、资源快照与引用保护 |
frontend/src/utils/、i18n.ts |
纯文本 / 数学转换与集中中英文案 |
frontend/worker/ |
应用外壳缓存;API 请求不进入离线缓存 |
backend/app.py |
HTTP / WebSocket 路由、任务调度与生成流程 |
backend/task_store.py、task_types.py |
任务持久化与数据契约 |
backend/symbol_fallback.py、math_*.py |
数学字形回退、几何与页面布局 |
backend/identify.py、pdf.py、logging_setup.py |
背景检测、PDF 导出与统一日志 |
desktop/main.js、preload.js |
后端进程、窗口、下载与隔离的渲染进程桥 |
desktop/studio-storage.js |
固定键存储 IPC、来源校验、原子文件写入与写队列等待 |
e2e/ |
Web 与真实桌面打包产物的端到端验证 |
界面将用户操作交给工作区业务入口;业务入口组合 store、API、预设仓库和资源服务。 API 层处理传输与数据契约,存储适配层处理持久化。store 保留状态与同步变更, 不承担网络请求、文件写入或对象 URL 的创建与释放。
状态归属¶
| 状态 | 所有者 | 保存策略 |
|---|---|---|
| 正文、插图映射、导入文件名 | document store | 工作区快照与二进制资源 |
| 手写参数、字体、背景、生成选项 | settings store | 工作区快照;上传文件保存为资源 |
| 语言、明暗模式、界面主题色、表格转换模式 | preferences store | 工作区快照 |
| 预览图片 URL、当前页、下载展示信息 | preview store | 内存展示状态;不保存为草稿 |
| 字体目录、版本、任务状态、原始生成结果 | TanStack Query | 内存缓存;不把整份 Query 缓存写入本地 |
| 生成任务 ID 与导出模式 | 生成流程 / studio.generation |
用于刷新后续接服务端任务 |
| 操作通知、保存状态 | 独立 store | 内存状态 |
| 弹窗、折叠、选区、数字框暂存文本 | 组件状态与 ref | 随组件生命周期结束 |
组件通过字段 selector 或 useShallow 订阅需要的值。正文输入不会通过一个巨大的
Context value 传播到整页;任务进度由状态展示组件单独订阅。Context 用于语言环境和
稳定的工作区操作入口,QueryClientProvider 提供服务端缓存。
生成数据流¶
- 业务入口读取正文和参数快照,先做本地校验;提交时将插图占位符展开为接口所需内容。
- API 层提交任务。后端将任务写入 SQLite,并返回任务 ID。
- 客户端保存任务 ID 与导出模式,优先通过 WebSocket 观测进度;连接不可用时回退到 HTTP 轮询。
- 推送和轮询更新同一份 Query 任务缓存。完成后获取图片、ZIP 或 PDF,再交给预览与下载适配器。
- 刷新时读取已保存的任务身份,继续观测原任务,不重新 POST。服务端任务必须仍在有效期内。
生成 POST 不自动重试;队列满时按服务端返回的期限禁用提交。取消、卸载和页面离开会 中止客户端请求并清理观测连接,不代表删除后端任务。暂时的网络错误或超时保留任务身份, 成功获取结果、明确失败或任务已不存在时清除对应记录。上传快照在 POST 结束后释放引用。
本地持久化¶
元数据与二进制分开保存¶
| 内容 | Web | 桌面 |
|---|---|---|
studio.workspace、预设、活跃任务记录 |
当前来源的 localStorage | 应用数据目录的 studio/records/ |
| 插图、上传字体、纸张背景 | IndexedDB 的 handwriting-studio / assets |
应用数据目录的 studio/assets/ |
桌面数据目录由主进程决定:Windows 为 %LOCALAPPDATA%/HandwritingWeb,macOS 为
~/Library/Application Support/HandwritingWeb。存储不依赖本次后端随机端口。
后端的 SQLite 任务队列与这里的编辑器草稿是不同的数据及生命周期。
studio.workspace 将正文、参数、偏好和资源引用放在一份快照中;预设使用独立记录。
插图与上传文件按内容哈希去重。快照保存资源 ID 和文件元数据,不保存 blob: URL,
也不把整份 Base64 图片塞进草稿记录;恢复时重新构造 File、插图映射和必要的对象 URL。
保存与恢复边界¶
- 普通编辑防抖保存,失焦、页面隐藏和离开时补写;已经准备好的资源允许同步提交 Web 元数据。
- 上传资源先持久化,再发布对应界面状态;存储不可用时仍保留内存中的编辑,并明确显示保存失败。
- 二进制先写入,再提交引用它的完整元数据快照。失败不会用半份新快照覆盖上一份完整草稿。
- 桌面文件通过临时文件、同步与重命名替换;同一记录的写入串行,读取等待此前已接收的写入。
- 启动时优先读取当前快照,缺少快照时读取旧
studio.*、camelCase 设置及myPreset。 恢复失败会提示,并阻止自动覆盖无法读取的工作区。 - 对象 URL 由资源服务管理,替换预览或背景时释放旧 URL;较慢的异步结果不能覆盖更新的编辑状态。
Web 存储按来源隔离,没有跨标签页的协作合并。为避免误删另一标签页的在途资源,Web 不自动回收 无引用的二进制;桌面只回收本次已识别且不被工作区、预设或在途操作保护的应用资源。 读取或保存失败时,本会话停止自动回收。
桌面退出¶
正常关窗或退出应用时,主进程先保留页面并请求保存,收到确认后等待已接收的写入队列,再退出。 保存失败或等待超时时可取消退出;强制结束进程或断电只能依赖此前已提交的快照。 存储桥仅接受当前主窗口主 frame 的精确同源请求,并限制记录键、资源 ID、大小和在途操作, 不向页面开放任意文件路径。
应用更新¶
客户端更新与服务部署更新分开处理:
| 环境 | 更新方式 |
|---|---|
| Electron Windows 安装版 | 用户选择「更新并重启」后,校验新旧安装包、保存工作区、退出,按当前用户静默安装并重启;启动失败时尝试用旧安装包恢复 |
| Electron Windows 便携版 | 保存并退出后原子替换外层 EXE,重启确认可用;失败则恢复备份 |
| Electron Apple 芯片 macOS | 校验 ZIP 内完整应用和签名后交换同卷应用目录,重启确认可用;失败则换回旧应用 |
| Web / PWA | 检查当前网站部署的 Service Worker 更新,保存草稿后由用户确认刷新;GitHub 客户端 Release 不代表当前网站已升级 |
发现新版本时弹窗展示更新要点;自动提醒按版本在本会话去重,手动检查可再次打开。启动检查可关闭,用户确认更新前不下载、不退出。一次「更新并重启」串联下载、准备、保存、退出、更新与重启;选择稍后会撤销自动重启意图。生成过程中等待任务结束,草稿保存失败时保留当前应用。当前无对应架构安装包或发布包尚未上传时,界面说明原因并提供官方 Release 入口。
渲染进程仅提交受限更新操作,主进程决定官方仓库、资产名与更新文件位置;不能通过更新 IPC 指定任意 URL、文件路径或命令。下载使用独立临时目录,取消与失败清理半包。安装前重新校验下载文件,沿用既有退出保存边界。
独立原生更新助手在退出前验证安装身份、写权限、备份和恢复入口;只有准备完成且确认旧主进程与后端退出后才修改安装。事务记录固定版本、随机标识及进程出生身份,重启后的后端版本、首帧与工作区恢复成功才确认更新完成。未确认的新进程会被精确停止,再恢复旧版;恢复失败时保留备份与诊断信息,不反复覆盖文件。用户资料目录不随安装目录替换。
只读目录、需要管理员权限的系统安装、无法建立恢复入口或系统不支持原子交换时保留手动安装。macOS 保留隔离属性和系统信任检查,不通过删除属性或重新签名绕过系统许可。NSIS 恢复使用旧版本的正式安装程序;外部安装器故障、磁盘或文件系统损坏无法保证自动恢复,界面提供明确反馈与官方安装入口。
扩展约定¶
- 新增客户端状态先确定所属业务域;临时交互状态留在组件,服务端数据进入 Query 缓存。
- 新接口在
api/中定义契约、校验和 Query key;跨域副作用由工作区业务入口组合。 - 新增保存内容通过持久化适配器,保留迁移与失败处理;扩展记录键时同步渲染端协议与桌面桥白名单。
- 改动刷新恢复、任务观测或退出保存时,覆盖相应单元测试与端到端路径。
- 精确字段、大小限制、超时和缓存参数以源码与测试为准;版本与提交历史不在本页重复维护。