Files

327 lines
25 KiB
Markdown
Raw Permalink 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.
# Xianren Studio(仙人工作室)代码组织与布局
> 本文档描述代码仓库的组织结构、模块职责、数据流与关键实现。**功能变更后如涉及结构、流程或数据模型,必须同步更新本文档。**
最后更新:2026-08-16
---
## 1. 总览
技术栈:Tauri 2Rust 壳 + WebView2 + React 18 / TypeScript / Vite / Tailwind,推理引擎为 llama.cpp 的 `llama-server` 子进程。
```
xianren_studio/
├── apps/desktop/ Tauri 桌面壳(Rust 命令层、tauri.conf.json、推荐模型/预制智能体/预制工作流默认 JSON、工作流执行引擎 workflow.rs、邮件发送 mail.rs、文档提取与目录扫描 knowledge.rs
├── crates/core/ 领域核心:SQLite(模型注册表、智能体、工作流、知识库、定时计划、会话、消息、设置)
├── crates/engine/ llama-server 生命周期 + 流式/非流式聊天(本地与远程 OpenAI 兼容)
├── crates/download/ 分片断点续传下载器
├── crates/api/ OpenAI 兼容本地 API 服务(axum
├── ui/ React 前端
├── scripts/ 构建 / 引擎与测试模型下载脚本
├── docs/ 产品与技术方案、功能记录、本文档
└── AGENTS.md 开发/维护约定(含文档同步规则)
```
---
## 2. 分层与数据流
```
React UIui/src
│ Tauri invokeapi.ts 封装)+ 事件监听(onEvent)
apps/desktop/src/commands.rs ← 所有 Tauri 命令(invoke_handler 注册于 lib.rs
├── crates/coreSQLitemodels / agents / workflows / scheduled_tasks / sessions / settings
├── crates/enginellama-server 子进程 / 远程 OpenAI API
├── crates/download(下载任务)
└── crates/api(本地 API 服务)
```
前端通过 `ui/src/api.ts``api.*` 方法调用后端命令;后端通过 Tauri `emit` 向前端推送事件(见 §7 事件通道)。
---
## 3. 前端结构(`ui/`
```
ui/src/
├── main.tsx 入口
├── App.tsx 导航栏、路由(10 个页面)、全局事件订阅(引擎/下载/模型/建议/定时计划/会话)
├── api.ts Tauri invoke 封装 + 全部请求/事件 TypeScript 类型
├── store.ts zustand 全局状态(见下)
├── styles.css 全局样式
├── components/Icon.tsx SVG 图标库
└── pages/
├── ChatPage.tsx 对话页(最大的页面,含会话快照、消息气泡、模型下拉、建议标签)
├── AgentsPage.tsx 智能体列表(预制/自定义分组、新建/编辑/删除/恢复预制)
├── AgentChatPage.tsx 智能体会话页(按路由加载智能体并渲染 ChatPage 智能体模式)
├── WorkflowsPage.tsx 工作流列表(预制/自定义分组、新建/删除/恢复预制)
├── WorkflowCanvasPage.tsx 工作流画布(节点拖入/拖拽/连线、滚轮缩放、空白平移、节点/连线右键菜单、导入导出、运行与结果展示)
├── KnowledgeBasePage.tsx 知识库(库管理、文档上传/预览/重切、FTS 检索与复制引用)
├── ModelsPage.tsx 模型管理(本地部署 + 在线 API 启用开关)
├── ModelPlazaPage.tsx 模型广场(HF/ModelScope 搜索与下载)
├── TasksPage.tsx 任务(下载/部署进度)
├── ToolsPage.tsx 工具(Tavily 联网搜索)
├── ServerPage.tsx 服务管理(本地 OpenAI API
└── SettingsPage.tsx 设置
```
### 3.1 全局状态(`store.ts`
主要状态字段:
| 字段 | 说明 |
| --- | --- |
| `agents` / `agentsLoaded` | 智能体列表(预制 + 自定义)与加载标记 |
| `agentConversations` | 智能体 id → 会话列表(各智能体会话隔离,key 为智能体 id) |
| `workflows` / `workflowsLoaded` | 工作流列表(预制 + 自定义)与加载标记 |
| `scheduledTasks` | 定时计划列表(任务页) |
| `models` | 模型列表(含 `enabled``kind``file_name` 等) |
| `engine` | 引擎状态(running / model / port |
| `deployStates` / `deployProgress` | 各模型部署状态(loading/ready/error)与进度 |
| `chatSuggestionsByMessage` | 消息 ID → 预测建议列表(全局接收,跨选项卡不丢) |
| `conversations` | 会话列表 |
| `tasks` | 下载/部署任务 |
| `server` | 本地 API 服务状态 |
### 3.2 会话快照(`ChatPage.tsx`
切换选项卡时 React Router 会卸载页面,因此 `ChatPage.tsx` 顶部维护了模块级 `chatSession` 对象(当前会话、所选模型、消息、输入草稿、请求参数、面板开合、工具、版本、附件),每次渲染后写回;重新挂载时用快照初始化,实现「切走再回来保持原状」。
---
## 4. 后端命令层(`apps/desktop`
### 4.1 `lib.rs`
- `run()`:初始化数据目录(`%APPDATA%\XianrenStudio`)、日志、panic 钩子;构建 Tauri 应用并注册 `invoke_handler`
- 注册插件:`tauri-plugin-opener``tauri-plugin-autostart`(开机启动,Windows 写注册表启动项)、`tauri-plugin-dialog`(原生文件/文件夹选择对话框,知识库目录来源「浏览…」使用)。
- `tauri.conf.json` 启用 `assetProtocol`scope `**`):知识库媒体文档(图片/视频/音频)预览通过 `convertFileSrc` 直读本地文件渲染/播放。
- `setup` 末尾启动**自动加载模型**任务(`auto_load_models`:按设置列表顺序逐个部署,最后一个保持运行)。
- `setup` 末尾启动**定时计划后台调度循环**tokio interval 每 30 秒调用 `run_due_scheduled_tasks`)。
- 数据目录:`logs/``models/``engines/``recommendations/`
### 4.2 `commands.rs`(命令分组)
| 分组 | 命令 |
| --- | --- |
| 智能体 | `list_agents``add_agent``update_agent``remove_agent``set_agent_enabled``restore_preset_agents` |
| 工作流 | `list_workflows``add_workflow``update_workflow``remove_workflow``set_workflow_enabled``restore_preset_workflows``run_workflow` |
| 定时计划 | `list_scheduled_tasks``add_scheduled_task``update_scheduled_task``remove_scheduled_task``set_scheduled_task_enabled``run_scheduled_task_now` |
| 邮件 | `mail_test`(用当前 SMTP 配置发送测试邮件) |
| 知识库 | `list_knowledge_bases``add_knowledge_base``update_knowledge_base``remove_knowledge_base``list_kb_documents`(分页)、`get_kb_document``kb_import_documents``remove_kb_document``kb_clear_documents``kb_rechunk_document``kb_search`(分页) |
| 知识库·目录来源 | `kb_list_sources``kb_add_source``kb_scan_sources`(后台扫描,`kb://scan-progress` 事件推送进度)、`kb_remove_source` |
| 应用/设置 | `app_info``autostart_status``autostart_set``settings_get``settings_set` |
| 模型 | `list_models``import_model``remove_model``set_model_enabled``scan_models``add_remote_model` |
| 模型广场 | `search_models``list_model_files``list_recommended_models``import_recommendations``fetch_model_page` |
| 会话 | `list_conversations`(可按 `agent_id` 过滤)、`create_conversation`(可选 `agent_id`,自动继承智能体系统提示词与默认模型)、`rename_conversation``set_conversation_pinned``set_conversation_favorite``import_conversation``set_conversation_tools``delete_conversation``get_messages` |
| 消息 | `chat_send``chat_stop``regenerate_message``edit_message``list_message_versions``apply_message_version` |
| 引擎 | `engine_start``engine_stop``engine_status``deploy_model` |
| 工具 | `web_search`Tavily)、`list_skills``add_skill``update_skill``remove_skill``set_skill_enabled``test_skill``list_mcp_servers``add_mcp_server``update_mcp_server``remove_mcp_server``set_mcp_server_enabled``mcp_test_server``mcp_list_tools``mcp_call_tool` |
| 下载 | `download_enqueue` |
| 服务 | `server_start``server_stop``server_status` |
| 其他 | `report_error``open_path` |
**聊天相关核心函数(均在 `commands.rs`):**
- `chat_send`:写入用户消息 → 构建历史 → 拉起后台生成任务。
- `run_generation_and_stream`:统一生成入口(本地引擎 / 远程 API),流式转发 token、统计用量、写库、发 `chat://done`,随后触发标题生成与建议生成。
- `inject_system_prompts`:把会话/智能体系统提示词(`conversations.system_prompt`)与工具说明统一注入消息历史最前;`chat_send` / `regenerate_message` / `edit_message` 三条链路共用。
- `maybe_generate_suggestions``call_suggestion_model``parse_suggestions`:回答完成后预测用户接下来可能说的话。统一要求模型输出 JSON 数组;最多重试 3 次;过滤元信息行与回答原文片段;每条 ≤ 30 字。
- `maybe_generate_conversation_title`:首轮对话自动生成标题。
- `create_conversation`:先在指定作用域(普通对话或某智能体)查找空会话(`find_empty_conversation`)复用,没有才新建;智能体会话写入该智能体的系统提示词与默认模型。
- `seed_preset_agents`:启动时首次写入 `default_agents.json` 中的预制智能体(`settings.preset_agents_seeded` 标记只执行一次);`restore_preset_agents` 命令可随时补齐缺失预制体。
- `run_workflow`:加载工作流节点/连线 → 交给 `workflow.rs` 执行引擎按拓扑顺序运行;LLM 节点经 `resolve_workflow_node_model` 解析模型(节点 → 工作流默认 → 运行参数 → 自动回退),`call_model_text` 非流式调用本地引擎或远程 API,逐节点推送 `workflow://node-status` 事件。
- `seed_preset_workflows`:启动时首次写入 `default_workflows.json` 中的预制工作流(`settings.preset_workflows_seeded` 标记只执行一次);`restore_preset_workflows` 命令可随时补齐缺失预制体。
- `run_due_scheduled_tasks`:后台调度入口,由 `lib.rs` 启动的 tokio 循环每 30 秒调用一次;先清理重启前中断的「运行中」任务,再取出到点的启用计划逐个标记并异步执行。
- `execute_scheduled_task` / `run_scheduled_task_once`:定时计划单次执行——解析执行智能体(指定智能体已删除时回退「通用助手」)与模型(`resolve_scheduled_model`:智能体默认模型 → 第一个本地模型 → 第一个已启用在线模型),在该智能体作用域内创建/复用空会话,写入用户消息并注入智能体系统提示词,本地模型未运行则自动拉起引擎,最后经 `call_model_messages` 调用模型并把助手回答写回数据库。
- `call_model_messages``call_model_text` 的通用版,可携带 system + 多轮消息调用本地引擎或远程 OpenAI 兼容 API(定时计划复用)。
- `auto_load_models`:应用启动时读取设置键 `auto_load_models`(JSON 数组,本地模型 id 按序排列),逐个调用 `start_engine_and_wait` 部署,非最后一个模型加载完即停止,最后一个保持运行。
- `start_engine_and_wait`:本地模型引擎启动的公共实现(构造 `EngineConfig`、推送 `engine://deploy` / `engine://deploy-progress` 事件、轮询加载进度、更新 `engine_base`);`deploy_model` 命令与 `auto_load_models` 共用。
- `autostart_status` / `autostart_set`:查询 / 设置开机启动(`tauri-plugin-autostart``ManagerExt`)。
- `run_scheduled_task_once`:大模型执行完成后,若任务开启邮件,按内容模式发送——固定模式用填写的主题/正文,LLM 模式用智能体回答作正文(主题留空取回答首行);邮件发送失败时任务标记失败但保留回答内容。
- `send_task_email` / `load_smtp_config` / `split_recipients`:读取设置页「邮件」SMTP 配置、拆分多收件人并调用 `mail.rs` 发送。
- `kb_import_documents`base64 解码 → `knowledge.rs::extract_text` 提取文本(文本类直接 UTF-8PDF 走 pdf-extract)→ `kb_db::chunk_text` 切块(按知识库 chunk_size/overlap)→ `insert_document_with_chunks` 事务写入文档与分块,逐文件返回成功/失败结果。
- `kb_search` / `kb_rechunk_document`:检索走 `kb_db::search`FTS5 trigram + BM25,短查询回退 LIKE);重切按知识库最新分块设置重建该文档分块与索引。
- `kb_add_source` / `kb_scan_sources` / `kb_remove_source`:目录来源注册与后台扫描——`knowledge.rs::collect_files` 按后缀过滤(空=全部)与递归开关收集文件,`classify` 分成文本 / 图片视频 / 音频三类(其他忽略),文本提取切块入库、媒体以文件名为内容登记,`kb_db::document_exists` 按(kb_id + file_path)去重;扫描过程逐文件推送 `kb://scan-progress`(处理数/成功/失败/忽略/分块),多目录重扫最后推送一次汇总 done;删除来源级联删除其导入文档。
**邮件发送模块(`apps/desktop/src/mail.rs`):**
- `send_email`:用 lettre 通过 SMTP 发送纯文本邮件;加密方式按设置分支——隐式 SSL 用 `relay`465)、STARTTLS 用 `starttls_relay`587)、无加密用 `builder_dangerous`25),均可自定义端口与超时。
**工作流执行引擎(`apps/desktop/src/workflow.rs`):**
- `topo_order`:Kahn 拓扑排序,检测循环 / 未知节点 / 自连。
- `resolve_template`:解析 `{{input}}``{{节点id}}` 变量。
- `execute_workflow`:按拓扑顺序执行 start / text / llm / end 节点,收集各节点输出并汇总最终结果;通过注入的 `call_model` 闭包调用模型,与具体模型解耦(单元测试用假模型跑通 5 个案例,另有真实模型端到端测试 `real_model_translation_workflow_e2e`)。
**工具协议(技能 / MCP):**
- `apps/desktop/src/tools.rs``ToolDef`、标记解析(`[[skill:名]]` / `[[mcp:服务:工具:参数]]`)、`MarkerFilter`(流式输出时过滤标记)、`run_tool_round`(执行工具并回填模型,最多 3 轮)、系统提示词构建。
- `apps/desktop/src/mcp_client.rs`MCP Streamable HTTP 客户端(`initialize` / `tools/list` / `tools/call`,兼容 SSE 响应)。
- 聊天链路:`chat_send` / `regenerate_message` / `edit_message` 解析会话工具并注入系统提示词 → `run_generation_and_stream` 流式输出时过滤标记 → 完成后执行工具循环并保存最终内容。
---
## 5. 核心库(`crates/core`
### 5.1 `app.rs`
- `CoreApp`:持有 SQLite 连接(`Mutex<Connection>`)与数据目录。
- `seed_default_settings`:启动时用 `insert_default` 补齐缺失的设置键(不覆盖用户已保存值)。
- `open_db`:执行 `schema.sql` + **增量迁移** `ensure_column`(为旧库补充新列,幂等)。
### 5.2 `schema.sql`(表结构)
| 表 | 关键字段 | 说明 |
| --- | --- | --- |
| `models` | `kind`local/remote)、`enabled``file_name``file_path``status``base_url``api_key``api_model``meta_json` | 模型注册表;`enabled` 决定在线 API 模型是否出现在聊天页可选列表 |
| `settings` | `key`/`value` | 键值设置 |
| `conversations` | `title``model_id``system_prompt``agent_id``pinned``favorite``tools_json` | 会话;`agent_id` 为空表示普通对话,否则属于对应智能体 |
| `agents` | `kind`preset/custom)、`name``icon``description``system_prompt``model_id``enabled` | 智能体(预制 + 自定义) |
| `workflows` | `kind`preset/custom)、`name``icon``description``nodes_json``edges_json``model_id``enabled` | 工作流(节点/连线以 JSON 存储,节点含 type/label/position/data |
| `scheduled_tasks` | `name``agent_id``prompt``interval_minutes``enabled``next_run_at``last_run_at``last_status`idle/running/success/error)、`last_result``last_error``email_enabled``email_to``email_mode`fixed/llm)、`email_subject``email_body` | 计划任务;`next_run_at` 由 SQLite `datetime('now', '+N minutes')` 计算(UTC),每次执行完成后推进 |
| `messages` | `role``model_id``content``tokens_in/out``elapsed_ms``first_token_ms``images_json` | 消息;`model_id` 记录该回答所用模型(回答下方展示模型名) |
| `message_versions` | `content``tokens_out``seq` | 重新生成前的旧版本 |
| `skills` | `name``description``content``enabled` | 技能工具库 |
| `mcp_servers` | `name``description``url``auth_token``enabled` | MCP 服务配置 |
| `knowledge_bases` | `name``description``chunk_size``chunk_overlap` | 知识库及分块设置 |
| `kb_documents` | `kb_id`(级联删除)、`name``file_type``file_size``char_count``chunk_count``content`(提取后的纯文本/媒体文件名)、`file_path``source_id` | 知识库文档;目录导入记录文件路径与来源,`file_path` 用于去重;删除/重切分块由 FTS 触发器同步 |
| `kb_chunks` | `kb_id``document_id`(级联删除)、`seq``content` | 文档分块 |
| `kb_chunks_fts` | FTS5 虚拟表(contentless-delete + trigram 分词) | 全文检索索引,由 `kb_chunks` 触发器维护 |
| `kb_sources` | `kb_id`(级联删除)、`path``extensions``recursive``(kb_id, path)` 唯一 | 目录来源(多个);添加时立即扫描,可重新扫描增量导入 |
迁移清单(`ensure_column`):`models.kind/base_url/api_key/api_model/enabled``messages.elapsed_ms/first_token_ms/images_json/model_id``conversations.pinned/favorite/tools_json/agent_id`
新增列迁移:`scheduled_tasks.email_enabled / email_to / email_mode / email_subject / email_body``kb_documents.file_path / source_id``ensure_column` 幂等补齐)。
### 5.3 `models.rs`
模型 CRUD、`set_enabled`(启用/停用)、`scan_directory`(扫描 GGUF 目录)、量化猜测 `guess_quant`
### 5.4 `sessions.rs`
会话/消息/版本 CRUD`find_empty_conversation`(新建对话复用空会话);`import_message`(导入对话用,可指定创建时间)。
### 5.5 `agents.rs`
智能体 CRUD`list/get/insert/update/delete/set_enabled`)与预制体批量写入 `insert_presets_if_missing`(按 id 忽略已存在,供首次种子与「恢复预制智能体」复用)。
### 5.6 `settings.rs`
`get/set/insert_default/all`
### 5.7 `workflows.rs`
工作流 CRUD`list/get/insert/update/delete/set_enabled`)与预制体批量写入 `insert_presets_if_missing`(按 id 忽略已存在);节点 `WorkflowNode`type=start/llm/text/end、position、data)与连线 `WorkflowEdge` 以 JSON 列存储,序列化/反序列化在读写时完成。
### 5.8 `scheduled_tasks.rs`
计划任务 CRUD 与运行状态落库:`list/get/insert/update/delete/set_enabled`(停用时清空 `next_run_at`,启用时重新计算)、`mark_running` / `finish_run`(写执行结果并推进下次执行时间)、`list_due`(到点且未运行中的启用计划)、`reset_stale_running`(应用重启后把中断的「运行中」标记为失败);任务行含邮件配置字段(`email_enabled/email_to/email_mode/email_subject/email_body`)。
### 5.9 `knowledge_base.rs`
知识库 CRUD`list/get/insert/update/delete`)、文档 CRUD`list/get/get_detail``count_documents` / `list_documents_page` 分页)、`insert_document_with_chunks`(文档 + 分块同事务写入,FTS 触发器自动建索引)、`delete_document`(级联删除分块并同步索引)、`document_exists`(目录导入按文件路径去重)、`rechunk_document``chunk_text`(按字符切块 + 重叠)、`search` / `count_search`FTS5 trigram BM25 排序 + 总数统计,查询 <3 字或 MATCH 失败回退 LIKE);目录来源 `list/get/upsert/delete_sources``delete_docs_by_source`
---
## 6. 引擎与远程调用(`crates/engine`
### 6.1 `manager.rs``EngineManager`
- `start`:以子进程启动 llama-server`CREATE_NO_WINDOW`),轮询 `/health` 等待就绪;最多 180s。
- `stop`:先请求 `/shutdown`,超时再 kill。
- `stream_chat` / `chat`:流式 / 非流式聊天(`/v1/chat/completions`)。
- `status`:返回 running / port / model。
### 6.2 `remote.rs`
- `stream_chat_remote` / `chat_remote`OpenAI 兼容远程 APIBearer 认证)。
- `normalize_base`Base URL 自动补 `/v1`
### 6.3 `types.rs`
- `ChatMessage`role/content/images,多模态时序列化为 content 数组。
- `ChatRequest`model / messages / temperature / top_p / max_tokens / stream。
- `ChatStreamEvent`Text / Reasoning(思考过程)/ Usage。
---
## 7. 事件通道(Tauri emit → 前端 onEvent
| 事件 | 方向 | 说明 |
| --- | --- | --- |
| `chat://token` | 后端→前端 | 流式增量文本 |
| `chat://reasoning` | 后端→前端 | 思考过程增量 |
| `chat://done` | 后端→前端 | 回答完成(含 message_id、model_id、用量) |
| `chat://suggestions` | 后端→前端 | 预测建议列表(全局订阅,存入 store) |
| `chat://tool-status` | 后端→前端 | 工具调用状态(running/done),界面提示 |
| `chat://message-updated` | 后端→前端 | 消息内容更新(恢复版本后) |
| `chat://title-updated` | 后端→前端 | 标题更新 |
| `chat://error` | 后端→前端 | 生成错误 |
| `engine://status` | 后端→前端 | 引擎状态变化 |
| `engine://deploy` | 后端→前端 | 部署状态(loading/ready/error |
| `engine://deploy-progress` | 后端→前端 | 部署进度百分比/阶段 |
| `models://updated` | 后端→前端 | 模型列表变化 |
| `download://started/progress/done/error` | 后端→前端 | 下载任务进度 |
| `server://status` | 后端→前端 | 本地 API 服务状态 |
| `workflow://node-status` | 后端→前端 | 工作流节点运行状态(running/done/error)与输出文本 |
| `scheduled://updated` | 后端→前端 | 定时计划状态变化(创建/编辑/启停/执行完成),前端刷新列表 |
| `conversations://updated` | 后端→前端 | 定时计划执行后会话列表变化(供智能体会话页刷新) |
| `kb://scan-progress` | 后端→前端 | 知识库目录扫描进度(处理/成功/失败/忽略/分块数;status=scanning/done |
---
## 8. 设置项(设置键 / 默认值)
定义于 `crates/core/src/app.rs``seed_default_settings`
| 键 | 默认值 | 用途 |
| --- | --- | --- |
| `model_dir` | 数据目录/models | 模型目录 |
| `engine_bin` | engines/cpu/llama-server.exe | llama-server 路径 |
| `backend` | auto | 后端(auto/cpu/cuda/vulkan |
| `hf_endpoint` | https://hf-mirror.com | 模型下载源 |
| `api_port` / `api_key` / `api_enabled` | 1234 / 空 / false | 本地 API 服务 |
| `upload_max_mb` | 10 | 上传大小上限 |
| `auto_title` | true | 自动生成标题 |
| `suggest_enabled` | true | 预测用户接下来说的话开关 |
| `suggest_count` | 3 | 预测条数(15 |
| `tavily_api_key` | (内置演示值) | 联网搜索 API Key |
| `tool_web_search_enabled` | true | 联网搜索工具开关 |
| `auto_load_models` | `[]` | 启动时自动加载的本地模型 id 列表(JSON 数组,顺序即加载顺序) |
| `smtp_host` / `smtp_port` / `smtp_tls` | 空 / 465 / wrapper | SMTP 发件服务器、端口、加密方式(wrapper/starttls/none |
| `smtp_user` / `smtp_password` | 空 / 空 | SMTP 用户名(通常为发件邮箱)与授权码/密码 |
| `smtp_from` / `smtp_from_name` | 空 / 仙人工作室 | 发件人地址(留空用用户名)与显示名称 |
| `recommend_dir` | 数据目录/recommendations | 推荐列表目录 |
| `preset_agents_seeded` | 首次启动后为 `1` | 预制智能体是否已写入(内部标记,避免覆盖用户删除) |
| `preset_workflows_seeded` | 首次启动后为 `1` | 预制工作流是否已写入(内部标记,避免覆盖用户删除) |
---
## 9. 构建与运行
```powershell
# 开发模式(自动拉起 Vite + Tauri
npm --prefix apps/desktop run dev
# 正式版(必须带 custom-protocol 特性,前端资源才会内嵌进 exe)
npm run build --prefix ui
cargo build --release -p xianren-desktop --features custom-protocol
# 或一键打包(tauri build 会自动启用该特性并打 NSIS 安装包)
npm --prefix apps/desktop run build
```
> 注意:不带 `custom-protocol` 编译出的 release exe **不会内嵌前端页面**,直接运行会白屏,因此正式发布必须带该特性。
---
## 10. 文档维护约定
- 功能变化 → 更新 `docs/FEATURES.md` 对应章节,并在「改动记录」追加。
- 结构/流程/数据模型变化 → 更新本文档对应章节。
- 大版本信息(技术栈、目录说明)变化 → 同步更新 `README.md`