refactor: 重构文档结构与环境配置,统一研发工单命名

1. 更新 .env 配置文件,替换原有云同步变量为 API 目标配置
2. 调整 publish-linux.ps1 中的文档路径,使用研发工单目录
3. 重构项目文档目录:将原 v1.2.0-tasks 迁移为研发工单-v1.2.0 目录,统一术语为"研发工单"替代"任务"
4. 更新 README.md 与各 PRD 文档的术语对照表,明确业务实体与研发工作项的区分
5. 新增多个研发工单文档,覆盖搜索、云同步、Linux 打包等模块
6. 删除旧的任务拆分文档,统一使用新的研发工单体系
This commit is contained in:
ShaoHua
2026-06-14 00:58:26 +08:00
parent a7ba814833
commit d81aa06681
18 changed files with 162 additions and 112 deletions
@@ -0,0 +1,51 @@
# Hua.Todo v1.2.0 研发工单总览
> 术语澄清:本目录下"工单 / 子工单 / 研发工单"指**编码工作项**;文中提到的"任务/任务列表/任务标题/父子任务/拉取任务/同步任务"等指 Hua.Todo 项目业务领域的 **Todo 待办项**(Task 实体),二者请勿混淆。详见 `.trae/rules/全局/05-研发工单规则.md`。
本目录用于把 [产品需求文档-1.2.0.md](file:///d:/Proj/6.Hua.Todo/docs/project/产品需求文档-1.2.0.md) 拆解为可落地、可并行推进的子工单。各工单文件之间尽量解耦;存在明确依赖时会在工单内标注。
## 目标(来自 PRD
- Linux 平台官方支持:MAUI 入口保持不动,Linux 新增 Avalonia 入口;继续用 WebView 承载同一套 Vue 前端;复用既有"本地 API ↔ 前端"的交互协议
- Todo 待办项检索(Search):主界面顶部新增搜索框,按 Todo 待办项标题模糊匹配
- 云同步(基础可用):用户手动配置服务端地址;RBAC + 二次认证;用户级数据隔离;服务端配置驱动的"可控落盘"
## 当前实现基线(用于工单定位)
- MAUI 启动与内嵌 WebServer`src/Hua.Todo.Maui`
- 前端(Vue)与 API client`src/Hua.Todo.Web`
- 后端宿主(ASP.NET,动态 API):`src/Hua.Todo.Host` + `src/Hua.Todo.Application/DynamicApi`
- 现有 WebView ↔ 前端注入协议(示例):`window.__API_BASE_URL__``window.mauiInterop`(用于 JS 侧拿到 API base 与若干事件桥接)
## 工单流(并行建议)
- **Linux 入口线**`01-*` + `02-*`
- 01:新增 Avalonia 入口、选择 Linux 可用的 WebView 控件、复用现有前后端协议
- 02:Linux 打包/交付产物(已落地:`.tar.gz` 发布脚本 + Flatpak 基础结构;详见 `publish-linux.ps1``pack/linux/`
- **Search 线**`03-*`
- 主要在前端完成;与云同步/平台入口基本无耦合,可并行
- 03:已完成:主界面搜索框(按 Todo 待办项标题包含匹配;命中即显示含上下文;Esc 清空;英文大小写不敏感)
- **云同步线**`04-*` + `05-*` + `06-*`
- 04:服务端基础能力(登录、Todo 数据同步/读取、配置下发、用户隔离)
- 05:客户端配置与同步工作流(手动指定服务端地址、登录后拉取 Todo 数据)
- 06:安全与落盘策略(RBAC、二次认证、可控落盘与"内存模式")
- **文档与验收线**`07-*`
- 对齐文档与现实现状/接口;补齐验收步骤与自测清单
## 待验证表
| 子工单 | 实现状态 | 验证状态 | 备注 |
|---|---|---|---|
| 01 - Linux 入口 | 已完成 | 待验证 | 基于 Avalonia + WebView.Avalonia 实现 |
| 02 - Linux 打包/交付 | 已落地 | 待验证 | `.tar.gz` 发布脚本 + Flatpak 基础结构 |
| 03 - Search 关键词检索 | 已完成 | 待验证 | 搜索框 + 树状 Todo 待办项标题包含匹配 |
| 04 - 云同步 服务端基础能力 | 已完成 | 已验证 | API 契约与错误码已固化到 04 文档 |
| 05 - 云同步 客户端配置与同步 | 已完成 | 待验证 | 新增"云同步设置"弹窗:地址校验+保存探测、登录/登出、登录后拉取云端 Todo 数据并在主界面只读展示 |
| 06 - 安全与落盘策略 | 未标注 | 待验证 | |
| 07 - 文档与验收 | 未标注 | 待验证 | |
## 交付判定(v1.2.0 Done Definition
- Linux:在基线发行版(建议 Ubuntu LTS)上可启动、可渲染前端、可调用本地 API;且有可安装/可运行的交付产物
- Search:可在主界面按 Todo 待办项标题实时过滤(含层级 Todo 的展示策略清晰)
- 云同步(基础可用):可配置服务端地址;登录后可拉取该用户的 Todo 数据;服务端可下发"是否允许落盘";客户端在禁止落盘时不产生本地持久化
@@ -0,0 +1,93 @@
# 01 - LinuxAvalonia 入口 + WebView 承载 Vue
## 目标
- 新增一个 **Linux 桌面端入口**Avalonia)作为 Hua.Todo 的 Linux 宿主
- 在 Linux 宿主中用 **WebView** 加载同一套 Vue 前端(与 MAUI 端共用前端构建产物与资源路径约定)
- 复用既有"前端 ↔ 本地 API"的交互方式(HTTP API + 现有注入变量/事件名),保证前端无需为 Linux 分叉
## 范围
- 新增/调整项目结构以容纳 Avalonia 入口(建议新项目:`src/Hua.Todo.Avalonia``src/Hua.Todo.Desktop.Avalonia`
- 在 Avalonia 中接入可在 Linux 运行的 WebView 控件(需要先调研与选型)
- 确保能正确加载:
- 内嵌 WebServer 的 `BaseUrl`
- 以及前端静态资源(开发态/生产态策略需明确)
## 依赖
- 依赖 `04-*`/`05-*` 的云同步工作不强依赖本工单,可并行
- 依赖前端已有构建产物或构建流程(至少能得到 `dist/` 或等效 `wwwroot/`
## 关键决策点(必须在实现前落定)
### 1) Avalonia/Linux 可用的 WebView 方案选型
候选方向(示例,最终以调研结果为准):
- WebKitGTK 系(Linux 依赖较重,但适配面广)
- Chromium 系(体积/依赖/许可成本需评估)
选型必须满足的最低能力:
- 基础导航与本地资源加载
- JS 执行/注入(用于对齐 `window.__API_BASE_URL__` 等契约)
- JS ↔ Native 双向通信(至少开发阶段可观测;可先只保证"HTTP API"路径通)
- 开发期可用 DevTools(至少能定位网络请求与控制台错误)
### 2) 资源加载策略(与 MAUI 对齐)
需要明确并实现两种模式:
- 开发态:Avalonia WebView 指向前端 dev server(例如 `http://localhost:5173`)或指向本地 WebServer
- 生产态:加载随应用交付的静态资源(`wwwroot`)并确保 SPA fallback 生效(访问非 `/assets/*` 的路由能回到 `index.html`
## 实现落地(已完成)
### 1) 项目结构
- 新增 Linux 桌面端入口项目:`src/Hua.Todo.Avalonia`
- 入口类型:Avalonia Desktop AppClassicDesktopLifetime
### 2) WebView 方案选型(已落定)
- 采用:`WebView.Avalonia` + `WebView.Avalonia.Desktop`
- Windows:依赖 WebView2 Runtime
- Linux:依赖 GTK + WebKitGTK(运行环境缺失时 WebView 会初始化失败)
### 3) 资源加载策略(与 MAUI 对齐)
- 生产态(默认):内嵌 WebServer 托管 `wwwroot` 静态资源,WebView 指向 `HostUrl`
- 开发态:将 `IsUsingStatic=false`WebView 指向 `ForEndUrl`(例如 Vite dev server
### 4) 前端契约对齐(与 MAUI 一致)
- 在 WebView 导航完成后注入:
- `window.__API_BASE_URL__ = "${HostUrl}/api"`
- `window.mauiInterop`(事件名/字段名与现有前端保持一致)
### 5) 本地 API 与数据库初始化
- 内嵌 WebServer 使用 Kestrel 启动并注册动态 API 与 Controllers
- 启动时执行数据库迁移(Migrate),并设置 SQLite WAL 模式以降低锁冲突风险
- 默认 SQLite 路径使用用户目录(`LocalApplicationData/Hua.Todo/Hua.Todo.db`),避免安装目录无写权限导致启动失败
## 实施步骤(建议)
1. 新建 Avalonia 宿主项目
- 提供 App/Window/MainView 基础结构
2. 接入 WebView 控件并完成最小加载闭环
- 能显示 `index.html`,能打开前端路由
3. 对齐与 MAUI 端一致的"前端契约"
- 注入 `window.__API_BASE_URL__`(值应为 `${BaseUrl}/api`
- 若沿用 `window.mauiInterop` 事件名,需在 Linux 端同名注入(建议命名逐步抽象为更通用的 `nativeInterop`,但 v1.2.0 以兼容为优先)
4. 复用本地 API(内嵌 WebServer
- 评估复用 `Hua.Todo.Host`/现有 Kestrel 组件的可能性
- 明确 Linux 端是否也走"内嵌 WebServer + WebView 指向 BaseUrl"的方式(推荐一致化,减少前端差异)
5. 输入法/字体/DPI 验证与可配置化
- 至少验证:中文输入法、缩放、Wayland/X11 下可用性
## 验收标准
- 在 Linux(建议 Ubuntu LTS)上启动 Avalonia 入口后:
- WebView 正常渲染前端主界面
- 前端能通过 `window.__API_BASE_URL__` 正确请求本地 `/api/*`(至少 Todo 待办项列表接口可用)
- 页面路由/刷新不会出现 404SPA fallback 生效)
- 开发态可调试(至少能看到控制台/网络错误,或能通过日志定位)
@@ -0,0 +1,40 @@
# 02 - Linux:打包、依赖与交付产物
## 目标
- 为 Linux 提供可安装/可分发的交付产物,并尽量做到"用户机器无需手动补大量依赖即可运行"
- 明确最低支持发行版范围(建议 Ubuntu LTS 作为基线)与依赖策略(WebView 运行时/字体/输入法等)
## 范围
- 构建脚本与产物输出
- 运行时依赖打包策略(尤其是 WebView 相关)
- 基础安装/运行说明(README / docs 更新)
## 依赖
- 依赖 `01-*`Avalonia 入口与 WebView 方案已定、并能在开发机运行)
## 交付形式(PRD 约束)
- 优先提供一种"自包含"官方安装方式(AppImage/Flatpak 二选一,建议先做一条跑通)
- 同时保留 `.deb``.tar.gz` 作为补充分发形式(以脚本产出为准)
## 实施步骤(建议)
1. 确定 Linux 发行版基线与运行时依赖清单
- 记录:最低 glibc、Wayland/X11、WebView 运行时依赖、字体包
2. 选择并落地一种自包含交付形式
- AppImage:偏"拎包即用",对依赖捆绑要求高
- Flatpak:沙盒与运行时生态更成熟,但需要 manifest 与权限策略
3. 补充 `.deb``.tar.gz`
- `.deb`:适合 Ubuntu/Debian 系;需要 desktop entry、图标、依赖声明
- `.tar.gz`:最通用;需要启动脚本与依赖说明
4. 增加启动前自检(可选但推荐)
- 发现关键依赖缺失时给出清晰错误提示与修复建议
## 验收标准
- 产物可在"干净环境"(尽量接近用户机)安装/运行
- 启动后 WebView 能渲染前端,且本地 API 可用
- 文档中给出的安装/运行步骤可复现,且与产物一致
@@ -0,0 +1,36 @@
# 解决方案:统一版本与打包分发
## 现状分析
- **版本不一致**`Hua.Todo.Maui` 的版本号为 `1.2.3`,而 `Hua.Todo.Avalonia``Hua.Todo.Host` 默认使用 `1.0.0`
- **打包脚本缺失**:目前仅 `Hua.Todo.Maui` 拥有 `setup.iss` (Inno Setup) 打包脚本,版本号硬编码为 `1.2.2`
- **配置冗余**:版本信息分散在各个 `.csproj``.iss` 文件中,维护困难。
## 目标
- 统一所有项目的版本号为 `1.2.3`
- 采用 `Directory.Build.props` 集中管理全局属性。
-`Hua.Todo.Avalonia` 提供与 `Hua.Todo.Maui` 一致的 Windows 安装程序打包能力。
## 实施方案
### 1. 统一 .NET 项目版本号
- 在项目根目录创建 `Directory.Build.props` 文件。
- 定义全局版本号 `<Version>1.2.3</Version>`
- 定义全局公司、版权、产品名称等元数据。
- 移除各 `.csproj` 中冲突的版本定义。
### 2. 统一 Inno Setup 打包脚本
- **更新 `src/Hua.Todo.Maui/setup.iss`**
- 将版本号更新为 `1.2.3`
- 确保安装路径与应用名称一致。
- **为 `src/Hua.Todo.Avalonia` 创建 `setup.iss`**
- 参考 Maui 的脚本,调整 `Source` 路径为 Avalonia 的发布路径:`bin\Release\net10.0\win-x64\publish\*`
- 调整可执行文件名称为 `Hua.Todo.Avalonia.exe`
- 调整 AppId 以避免与 Maui 版本冲突。
### 3. 发布流程标准化
- 以后发布时,只需修改根目录下的 `Directory.Build.props` 即可同步所有项目的版本。
- 执行 `dotnet publish -c Release -r win-x64` 后,手动或通过脚本运行 `ISCC setup.iss` 生成安装包。
## 预期效果
- 所有程序集(DLL/EXE)的版本号均显示为 `1.2.3`
- 提供 `Hua.Todo_Avalonia_Setup_v1.2.3.exe``Hua.Todo_Maui_Setup_v1.2.3.exe` 两个安装包。
@@ -0,0 +1,46 @@
# 03 - SearchTodo 待办项标题关键词检索
> 术语:本工单所指"Todo 待办项 / 任务标题 / 父子任务"为业务实体(Task / SubTask);"工单"指本研发工单本身。
## 目标
- 在主界面顶部增加搜索框
- 支持按 Todo 待办项标题进行模糊匹配(实时过滤)
## 范围
- 前端 UI:输入框、清空按钮、键盘交互(Esc 清空、Enter 可选)
- 过滤逻辑:对"树状 Todo(父子任务)"给出明确策略
- 性能:Todo 数据量增大时仍保持可用(至少避免 O(n^2) 的明显退化)
## 依赖
- 不依赖 Linux/Avalonia 或云同步,可独立并行完成
## 过滤策略(需在实现时确定,并写入实现说明)
树状 Todo(父子任务)常见两种策略,任选其一并保持一致:
1. **命中即显示(含上下文)**
- 任意节点标题命中则显示该节点
- 若子任务命中,可同时显示其祖先链(便于理解层级)
2. **扁平化命中**
- 只显示命中的 Todo(可能丢失层级语义)
建议优先采用"命中即显示(含上下文)",用户可更快定位。
## 实施步骤(建议)
1. 确定搜索框放置位置(主界面顶部)
2. 增加 `searchQuery` 状态与派生 `filteredTasks`
3. 将过滤逻辑接入现有列表渲染
4. 补充交互细节
- 输入时实时过滤
- 清空按钮
- Esc 清空、保持输入焦点(可选)
## 验收标准
- 输入关键字后,Todo 列表实时过滤,仅展示命中结果(符合上述策略)
- 清空后恢复原列表
- 对中英文与大小写的处理行为明确(至少:英文大小写不敏感;中文按包含匹配)
@@ -0,0 +1,149 @@
# 04 - 云同步(基础可用):服务端基础能力
> 术语:本工单文件本身归属"研发工单";文中"任务/Todo 数据"指业务实体(Task / SubTask)。
## 目标(PRD 约束)
- 支持用户级 Todo 待办项数据同步/读取(用户隔离)
- 提供基于角色/权限的访问控制(RBAC)
- 对高风险操作支持二次认证(能力以服务端实现为准)
- 下发"可控落盘"配置(服务端配置驱动)
## 范围(建议最小闭环)
- 认证(登录/会话)
- Todo 数据 API(按用户隔离)
- 配置下发 API(是否允许落盘、终端可信度/会话策略等)
- RBAC 最小落地(至少能区分"允许同步/禁止同步"或"只读/读写"
- 二次认证最小落地(至少覆盖"启用/关闭同步、切换账号、调整落盘策略"等高风险操作的接口)
## 依赖
-`05-*`(客户端)可并行推进,但需要尽早冻结 API 契约(字段名/响应结构/错误码)
## 接口契约(需要在实现前先写清楚)
v1.2.0 已实现一套最小可用契约(用于 05/06 客户端对接时冻结字段名/错误码)。
### 通用约定
- Base URL:由客户端配置(例如 `https://{host}:{port}`
- 认证:`Authorization: Bearer {accessToken}`
- 错误响应(统一结构):
```json
{ "code": "ERROR_CODE", "message": "Human readable message." }
```
### 错误码
- `UNAUTHORIZED`:未登录或会话失效
- `FORBIDDEN`:权限不足(RBAC / 策略拒绝)
- `SECOND_FACTOR_REQUIRED`:需要二次认证(step-up
- `BAD_REQUEST`:请求参数不合法
- `NOT_FOUND`:资源不存在
### Auth
- 初始化管理员(仅当系统尚无云用户时允许一次):
- `POST /auth/bootstrap`
- Body`{ "userName": "admin", "password": "..." }`
- 200:成功;403:已初始化或不允许
- 登录:
- `POST /auth/login`
- Body`{ "userName": "admin", "password": "..." }`
- 200`{ accessToken, expiresAtUtc, userId, role, permissions[] }`
- 二次认证(step-upv1.2.0 最小实现为"复用登录口令进行再认证"):
- `POST /auth/step-up`
- Header`Authorization: Bearer ...`
- Body`{ "password": "..." }`
- 200`{ stepUpExpiresAtUtc }`
### Tasks
- 获取当前用户 Todo 待办项全量:
- `GET /tasks`
- Header`Authorization: Bearer ...`
- 权限:`tasks:read`
- 200`CloudTaskItem[]`
### Sync
- 上传/同步(增删改后返回最新全量):
- `POST /sync`
- Header`Authorization: Bearer ...`
- 权限:`sync:write`
- 二次认证:必需(否则返回 `SECOND_FACTOR_REQUIRED`
- Body
```json
{
"upserts": [
{ "id": 1, "title": "A", "priority": 1, "isCompleted": false, "parentTaskId": null },
{ "id": null, "title": "New", "priority": 1, "isCompleted": false, "parentTaskId": null }
],
"deletes": [2, 3]
}
```
- 200
```json
{
"serverTimeUtc": "2026-04-06T17:39:30.0281279Z",
"tasks": [ /* CloudTaskItem[] */ ]
}
```
### Security Policy
- 获取安全策略(服务端下发):
- `GET /security/policy`
- Header`Authorization: Bearer ...`
- 权限:`policy:read`
- 200
```json
{
"allowPersist": true,
"allowSync": true,
"requireSecondFactorFor": ["sync:write", "policy:write"]
}
```
- 更新安全策略(高风险操作):
- `PUT /security/policy`
- Header`Authorization: Bearer ...`
- 权限:`policy:write`
- 二次认证:必需(否则返回 `SECOND_FACTOR_REQUIRED`
- Body`{ "allowPersist": false, "allowSync": false }`
## 关键实现点(建议)
详细实现方案请参考:[06.1-CloudSync-服务端安全设计方案.md](file:///d:/Proj/6.Hua.Todo/docs/project/研发工单-v1.2.0/06.1-CloudSync-服务端安全设计方案.md)
1. 用户隔离
- 服务端所有读写必须绑定当前登录用户上下文
2. 权限模型(RBAC
- 定义最小角色:例如 `user``admin`
- 定义最小权限:例如 `tasks:read``tasks:write``sync:enable`
3. 二次认证
- 选择实现方式(示例):二次口令/一次性验证码(TOTP)/短信(若无能力则先实现"二次口令")
- 能力不足时必须返回明确错误,使客户端可提示用户
4. 落盘策略下发
- 支持按用户或按会话/终端下发
- 默认策略建议为"允许落盘",便于可用性;但需支持"禁止落盘"
## 验收标准
- 使用不同用户登录后获取 Todo 数据,数据严格隔离
- 未授权角色/权限访问受限接口会被拒绝(错误响应可被客户端识别)
- 能返回安全策略配置(至少包含 `allowPersist`),并可通过配置切换行为
## 当前实现说明(v1.2.0
- 数据隔离:`Tasks` 表新增 `UserId` 外键,云端 API 的所有读写按当前会话用户隔离;本地模式使用固定的 `local` 用户 ID(不影响既有 Dynamic API)。
- RBAC:内置 `admin / user / readonly / nosync` 角色与权限映射;并叠加 `SecurityPolicies.AllowSync` 作为"是否允许同步写入"的策略开关。
- 二次认证:通过 `POST /auth/step-up` 将会话提升到 step-up 状态;`POST /sync``PUT /security/policy` 会强制要求 step-up。
@@ -0,0 +1,49 @@
# 05 - 云同步(基础可用):客户端配置与同步工作流
> 术语:本工单文件归属"研发工单";文中"任务/Todo 数据"指业务实体(Task / SubTask)。
## 目标(PRD 约束)
- 首次启用同步时,用户需要**手动填写服务端地址**(例如 `https://example.com`),并允许后续在设置中修改
- 客户端对地址格式做基础校验,并在保存时提示可达性/证书异常等风险信息
- 用户登录成功后,从服务端获取该用户 Todo 数据并更新到前端展示
## 范围(建议最小闭环)
- "同步设置"入口与界面(服务端地址、登录、同步开关)
- 地址校验与风险提示
- 登录后拉取 Todo 数据并刷新 UI
- 与本地数据的关系(v1.2.0 建议先做到"服务端为准/或本地为准"的单一策略,避免引入复杂冲突解决)
## 依赖
- 依赖 `04-*` 提供可用的服务端接口(至少登录 + 获取 Todo 数据 + 获取安全策略)
-`03-*`Search)互不影响,可并行
## 关键交互与状态
需要定义并贯穿实现的状态机(至少包含):
- 未配置服务端地址
- 已配置未登录
- 已登录(可同步)
- 同步中/同步失败/同步成功(含失败原因)
## 实施步骤(建议)
1. 增加"同步设置"UI
- 放置在主界面可发现位置(例如顶部右侧设置按钮/侧边栏)
2. 服务端地址配置
- 基础校验:`https?://`、host 合法性、尾部 `/` 处理规则
- 保存时探测:尝试请求服务端健康检查或登录端点(并提示证书异常/不可达等)
3. 登录与凭据存储策略(与 `06-*` 协同)
- 在允许落盘场景下可持久化 token/会话
- 在禁止落盘场景下仅保留内存会话(退出即失效)
4. 登录后拉取 Todo 数据
- 拉取成功后更新前端 Todo 列表
- 拉取失败给出可理解提示,并允许重试
## 验收标准
- 首次启用同步必须先配置服务端地址;地址非法时不可保存并提示原因
- 保存地址时能提示"不可达/证书异常"等风险信息(至少能区分:成功、失败、存在风险但可继续)
- 登录后能从服务端拉取 Todo 数据并展示在主界面
@@ -0,0 +1,58 @@
# 06 - 云同步(基础可用):安全(RBAC/二次认证)与"可控落盘"
> 术语:本工单文件归属"研发工单";文中"任务/Todo 数据"指业务实体(Task / SubTask)。
## 目标(PRD 约束)
- 高风险操作支持二次认证(例如启用/关闭同步、切换账号、调整"是否允许落盘"等)
- 客户端读取服务端安全配置,决定 Todo 数据是否允许落盘
- 当服务端标记"终端不可信/不允许落盘"时,客户端只能在内存中持有 Todo 数据;退出应用后不保留 Todo 数据
## 范围
- 客户端:落盘策略切换(持久化 ↔ 内存)、高风险操作二次确认/二次认证 UI
- 服务端:策略下发 + 二次认证挑战/校验接口(与 `04-*` 协作)
## 依赖
- 依赖 `04-*` 提供策略下发与二次认证能力(接口契约)
- 依赖 `06.1-*` 提供服务端底层安全实现(账户/密码/Token/策略存储)
- 依赖 `05-*` 的基础登录/同步 UI 入口
## 关键设计点(v1.2.0 必须明确)
### 1) "落盘"定义与边界
需要明确"哪些数据算落盘",并在禁止落盘时全部避免:
- Todo 数据(列表/详情)
- 同步状态(上次同步时间、待同步队列等)
- 登录凭据(token/refresh token/会话标识)
### 2) 本地存储抽象
建议把前端(或宿主)本地存储封装为可替换实现:
- 允许落盘:使用现有 localStorage/SQLite 等
- 禁止落盘:使用内存实现(刷新/退出即清空)
要求:
- 切换策略时行为可预期(例如从允许 → 禁止:立即清空已落盘数据并提示用户)
- 不在日志或 UI 中暴露敏感信息
### 3) 二次认证(挑战-响应)
建议采用"服务端驱动"的挑战流程:
- 客户端发起高风险操作
- 服务端返回"需要二次认证"的响应(包含 challenge 信息)
- 客户端弹出二次认证输入(例如二次口令/一次性验证码)
- 客户端携带证明再次提交
若服务端暂不支持二次认证,也需有"能力探测/降级提示",避免客户端卡死。
## 验收标准
- 服务端返回 `allowPersist=false` 时:
- 客户端不会把 Todo 数据与凭据写入任何持久化介质
- 退出应用后重新进入,Todo 数据为空(需重新登录/拉取)
- 对指定高风险操作:
- 无二次认证证明时被拒绝,并提示需要二次认证
- 提供正确二次认证后操作成功
@@ -0,0 +1,180 @@
# 06.1 - 云同步(安全进阶):服务端账户/凭据与策略实现方案
## 1. 目标
`04-*`(基础能力)与 `06-*`(落盘策略)提供服务端底层安全实现的具体方案,确保账户密码存储、Token 管理、二次认证及安全策略下发具备生产级安全性。
## 2. 账户与密码安全存储
服务端不应存储明文密码,需采用强哈希算法进行加盐处理。
### 存储结构 (Users 表)
- `UserId`: `Guid` (主键)
- `UserName`: `string` (唯一索引)
- `PasswordHash`: `string` (存储哈希后的结果)
- `PasswordSalt`: `string` (若哈希算法自带 Salt,如 BCrypt/Argon2,则可省略)
- `Role`: `string` (用户角色,如 admin/user)
- `CreatedAtUtc`: `DateTime`
- `UpdatedAtUtc`: `DateTime`
### 哈希算法建议
- **算法**: `Argon2id` (推荐) 或 `BCrypt`
- **配置**: 迭代次数、内存占用、并行度应符合 OWASP 最新建议。
***
## 3. Token 管理 (JWT)
采用 JSON Web Token (JWT) 作为会话凭据,并支持细粒度权限控制。
### Token 结构 (Payload)
```json
{
"sub": "UserId",
"name": "UserName",
"role": "Role",
"perms": ["tasks:read", "sync:write", "policy:read"],
"iat": 1712000000,
"exp": 1712003600,
"iss": "Hua.Todo.Server",
"aud": "Hua.Todo.Client",
"isStepUp": false // 是否通过了二次认证
}
```
### 校验逻辑
- 每次请求需携带 `Authorization: Bearer {token}`
- 服务端验证签名、有效期(exp)、签发者(iss)及受众(aud)。
- 权限校验:中间件检查 `perms` 声明是否包含当前接口所需的权限。
***
## 4. 二次认证与会话提升 (Step-up)
针对高风险操作(如 `sync:write``policy:write`),要求会话必须处于"提升状态"。
### 流程设计
1. **触发**: 客户端调用 `/sync`,服务端检查 Token 的 `isStepUp` 声明。
2. **拒绝**: 若 `isStepUp == false`,返回 `403 SECOND_FACTOR_REQUIRED`
3. **挑战**: 客户端调用 `/auth/step-up`,提供二次认证凭据(如再次输入密码,或 TOTP 验证码)。
4. **提升**: 验证成功后,服务端颁发一个新的 Token,其中 `isStepUp: true`,且有效期较短(如 30 分钟)。
5. **重试**: 客户端携带新 Token 重新发起请求。
***
## 5. 安全策略 (Security Policy) 存储与下发
安全策略决定了客户端的"落盘"行为及二次认证频率。
### 策略定义 (SecurityPolicies 表)
- `Id`: `int` (主键)
- `UserId`: `Guid` (外键)
- `AllowPersist`: `bool` (是否允许客户端持久化 Todo 数据)
- `AllowSync`: `bool` (是否允许该用户同步)
- `SecondFactorExpiryMinutes`: `int` (二次认证状态保持时长,默认 30)
- `IsTrustedDeviceOnly`: `bool` (是否仅限受信任终端)
### 下发逻辑
- 客户端通过 `GET /security/policy` 获取。
- 策略应与 `UserId` 绑定,允许管理员针对不同用户/角色进行差异化配置。
***
## 6. 审计日志 (Audit Logs)
记录关键安全事件,便于追溯。
- 登录成功/失败(记录 IP、终端信息)。
- 高风险操作尝试(是否通过二次认证)。
- 安全策略变更。
***
## 8. 客户端 UI 与交互设计
### 8.1 二次认证 (Step-up) 交互流程
1. **静默拦截**: 当用户触发高风险操作(如点击"立即同步"且 Token 已过期或未提升)时,客户端拦截请求并检测到 `403 SECOND_FACTOR_REQUIRED`
2. **弹出对话框**: 弹出"安全验证"模态框。
- **标题**: 需要二次认证。
- **描述**: "为了保护您的数据安全,执行此操作需要验证身份。"
- **输入**: 密码输入框(或 TOTP 验证码输入框)。
- **操作**: \[取消] \[验证并继续]。
3. **状态保持**: 验证成功后,UI 应显示短暂的"验证成功"提示,并自动重试刚才被拦截的操作。
### 8.2 客户端 UI (针对普通用户)
在客户端"设置 -> 云同步 -> 安全"路径下:
- **落盘策略 (AllowPersist)**:
- **展示**: 状态开关(Toggle)。
- **交互**:
- 若由服务端强制禁止,开关应为禁用状态(Disabled),并附带说明:"受服务端策略限制,当前终端禁止落盘"。
- 若允许修改,关闭开关时应弹出强提醒:"关闭后,所有本地 Todo 数据将被立即清除,退出应用后数据将不再保留。确定继续吗?"
- **二次认证频率**:
- **展示**: 显示当前二次认证的有效期(如 30 分钟)。
### 8.3 "不可落盘"模式下的视觉提示
`allowPersist == false` 时:
- **状态栏/标题栏**: 增加"无痕模式"或"内存存储"小图标/文字提醒。
- **登录页**: 增加提醒:"当前环境配置为禁止落盘,数据仅在本次运行期间有效"。
- **退出应用**: 点击退出时,若有未同步数据,强提醒:"数据未同步且本地禁止落盘,退出将导致数据丢失。确定退出吗?"
***
## 9. 服务端管理后台 (Admin Dashboard)
为了避免直接操作数据库,服务端需提供一套 Web 管理后台,供管理员维护账户、权限与安全策略。
### 9.1 工程位置与实现
- **宿主项目**: [Hua.Todo.Host](file:///d:/Proj/6.Hua.Todo/src/Hua.Todo.Host)
- **实现方式**:
- 前端采用 **Vue 3 + Vite** 开发。
- 静态资源在构建后嵌入到 `Hua.Todo.Host``wwwroot` 目录或作为内嵌资源。
- 后端通过 ASP.NET Core 的 `UseStaticFiles()` 托管,并确保 `/admin` 路由指向前端入口。
### 9.2 系统初始化 (Bootstrap)
- **触发条件**: 当检测到数据库中无任何用户时,访问 `/admin` 自动跳转至 `/admin/bootstrap`
- **功能**: 创建首个"超级管理员"账号。
- **UI**: 简单的表单(用户名、密码、确认密码)。
### 9.3 用户管理 (User Management)
- **用户列表**: 展示所有已注册用户(UserId, UserName, Role, CreatedAt)。
- **创建用户**: 管理员手动添加用户(用于内部系统或受控注册)。
- **重置密码**: 为忘记密码的用户生成临时密码或直接重设(需审计记录)。
- **禁用/删除**: 软删除或禁用账号。
### 9.4 安全策略配置 (Security Policy Management)
- **全局策略**: 设置系统默认的 `allowPersist``allowSync``SecondFactorExpiry`
- **单用户覆盖**: 在用户详情页中,管理员可以针对特定高风险用户/终端覆盖全局策略(例如:对特定外包人员账号强制 `allowPersist = false`)。
### 9.5 会话与凭据管理 (Session Management)
- **在线会话查看**: 展示当前所有有效的 Token/会话(记录 IP、最后活跃时间、是否已提升权限)。
- **强制下线 (Revoke)**: 管理员可一键吊销特定用户的所有 Token(将其加入黑名单)。
### 9.6 审计日志查看器 (Audit Log Viewer)
- **过滤查询**: 按时间、用户、事件类型(登录、同步、策略变更)过滤。
- **高亮显示**: 对"二次认证失败"、"异常 IP 登录"等敏感事件进行红色高亮。
***
## 10. 约束与风险
- **Token 吊销**: 默认 JWT 是无状态的。若需支持强制下线,需引入 Redis/DB 黑名单。
- **HTTPS**: 所有安全通信必须基于 TLS,防止中间人攻击窃取 Token。
- **暴力破解**: 需在 `POST /auth/login` 接口增加限流(Rate Limiting)机制。
@@ -0,0 +1,40 @@
# 07 - 文档同步与验收清单(v1.2.0)
> 术语:本工单文件本身归属"研发工单";文中"任务/Todo 待办项"指业务实体(Task / SubTask)。
## 目标
- 保证 v1.2.0 实现后,README 与 docs 中的说明与实际代码一致
- 为 Linux / Search / 云同步提供可复现的验收步骤
## 范围
- READMEFeatures、运行方式、API 说明(如涉及)
- docs:技术设计文档/技术栈与模块/版本记录(如涉及)
- 本目录工单文件:保持与实现同步(必要时更新验收口径与依赖关系)
## 必做同步点(结合当前仓库现状)
- **API 端点与项目结构**:对齐实际实现(避免文档仍描述不存在的项目或旧端点)
- **Linux 入口说明**:新增 Avalonia 入口的构建/运行/打包方式
- **云同步说明**:服务端地址配置、登录、落盘策略与安全机制(RBAC/二次认证)的用户可理解描述
- **版本记录**:在 `docs/版本记录.md` 增加 v1.2.0 非琐碎变更条目
## 验收清单(建议逐项勾选)
### Linux
- [x] Linux 上可启动并打开主界面
- [x] WebView 能渲染 Vue 前端且路由可用(刷新不 404)
- [x] 前端能成功请求本地 `/api/*` 并加载 Todo 待办项列表
- [x] 有至少一种自包含交付产物(AppImage/Flatpak 二选一)可运行(见 pack/linux/README.md
### Search
- [x] 主界面有搜索框
- [x] 输入关键字后列表实时过滤(层级策略符合 `03-*` 定义)
- [x] 清空后恢复原列表
### 云同步(基础可用)
- [x] 可手动配置服务端地址,并有基础校验与风险提示
- [x] 登录后能拉取该用户的 Todo 数据并展示
- [x] 服务端策略 `allowPersist=false` 时客户端不落盘,退出后数据不保留
- [x] 高风险操作触发二次认证(服务端支持时)
@@ -0,0 +1,332 @@
## 云同步设置 — 服务端化改造方案
### 1. 当前架构概述
#### 1.1 项目层次关系
```
src/
├── Hua.Todo.Application/ ← 共享层(服务端 & 客户端公用)
│ ├── CloudSync/ ← 云同步业务逻辑
│ │ ├── Auth/ ← 认证/授权
│ │ ├── Models/ ← DTO(纯数据合约,两端可用)
│ │ └── Services/ ← 业务服务
│ ├── Data/ ← EF Core DbContext
│ └── DynamicApi/ ← 动态 API 中间件
├── Hua.Todo.Host/ ← ASP.NET 服务端(云同步接口的提供方)
│ └── Program.cs
│ ├── AddApplicationServices() ← Todo CRUD 服务
│ └── AddCloudSyncServer() ← 云同步认证/授权/端点 **仅服务端**
└── Hua.Todo.Maui/ ← 桌面/移动客户端(嵌入 WebView)
└── MauiProgram.cs
└── AddApplicationServices() ← **仅** Todo CRUD 服务
(不注册 AddCloudSyncServer,不暴露云同步端点)
```
#### 1.2 两种部署模式
**模式 A:Host 独立服务端(开发/部署)**
```
┌───────────────────────────────┐
│ Vue 前端 (Vite :5174) │
│ cloudClient │
│ baseURL = serverUrl(外部) │ 直连外部
│ → /auth/login │ ────────→ 外部云同步服务
│ probeReachability() │ 直连外部
│ → fetch(serverUrl) │ ────────→
├───────────────────────────────┤
│ proxy /api │
├───────────────────────────────┤
│ ASP.NET Host (:5173) │
│ AddApplicationServices() │ Todo CRUD
│ AddCloudSyncServer() │ 云同步端点 ← **服务端**
│ ├─ /api/* (本地 Todo) │
│ └─ /auth/* (云同步) │
│ /tasks/* │
│ /cloud-sync/probe (新增) │
└───────────────────────────────┘
```
**模式 BMAUI 嵌入式 WebView(桌面端/移动端)**
```
┌───────────────────────────────┐
│ MAUI WebView │
│ ┌─────────────────────────┐ │
│ │ Vue 前端 (静态托管) │ │
│ │ cloudClient │ │
│ │ baseURL = 同源 │ │ 同源请求
│ │ → /api/* (本地 Todo) │──┼───────→ Embedded WebServer
│ │ → /auth/* (❌ 不可用) │ │ (不暴露云同步端点)
│ └─────────────────────────┘ │
├───────────────────────────────┤
│ Embedded WebServer (:5057) │
│ AddApplicationServices() │ 仅 Todo CRUD
│ AddCloudSyncServer() │ ❌ 未注册
│ UseDynamicApi() │ 仅本地 Todo API
│ MapCloudSyncEndpoints() │ ❌ 未映射
└───────────────────────────────┘
```
> **关键区别**`Hua.Todo.Application` 被两方引用,但云同步能力**仅在 Host 端通过 `AddCloudSyncServer()` 激活**。MAUI 端只使用 `AddApplicationServices()` 获取 Todo CRUD 能力,**不暴露也不应暴露云同步端点**。
#### 1.3 核心问题
1. 前端的"保存并探测"使用**浏览器端直接 `fetch()`** 探测外部 URL,受 CORS、证书、网络隔离限制。
2. 前端的"登录"通过 `cloudClient` 直连外部 `serverUrl`session token 存在前端 `sessionStorage`,与 Host 脱节。
3. 在 Host 模式下,Host 本身就是云同步服务,但前端并不知道,额外配置了外部 `serverUrl`
### 2. 改造目标
| 操作 | 当前 | 目标 |
|---|---|---|
| **保存并探测** | 浏览器 `fetch()` 直连 | 前端 → Host 接口 → Application CloudProbeService → 返回结果 |
| **登录** | 前端直连 `serverUrl` | 前端 → Host `/auth/login` |
| **后续云同步请求** | `cloudClient` 直连 `serverUrl` | 统一走 Host(同源请求) |
### 3. 变更详情
#### 3.1 新增:服务端探测接口
> **注意**`CloudProbeService` 和服务端点注册均在 `AddCloudSyncServer()` 路径下,MAUI 端不受影响。
**Application 层新增 `CloudProbeService`**
位置:`src/Hua.Todo.Application/CloudSync/Services/CloudProbeService.cs`
职责:接收目标 URL,从服务端发起 HTTP 探测,返回探测结果。
**新增 DTO**(位置:`src/Hua.Todo.Application/CloudSync/Models/AdminDtos.cs`,或新建 `CloudSync/Models/ProbeDtos.cs`
DTO 是纯数据合约,放在 Application 共享层无副作用,MAUI 端即使不调用也不会产生依赖。
```csharp
/// <summary>
/// 服务端探测请求。
/// </summary>
public class ProbeRequest
{
public string TargetUrl { get; set; } = string.Empty;
}
/// <summary>
/// 服务端探测响应。
/// </summary>
public class ProbeResponse
{
public bool IsReachable { get; set; }
public int? HttpStatus { get; set; }
public bool IsHttps { get; set; }
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
/// <summary>
/// success | warn | error
/// </summary>
public string Type { get; set; } = "error";
}
```
**DI 注册(`CloudSyncServiceCollectionExtensions.AddCloudSyncServer()`**
`AddCloudSyncServer()` 方法末尾追加,确保 `CloudProbeService` 仅在服务端 DI 容器中可用:
```csharp
// CloudSyncServiceCollectionExtensions.AddCloudSyncServer() 末尾新增:
services.AddHttpClient("ProbeClient", client =>
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("HuaTodo-Probe/1.0");
});
services.AddScoped<CloudProbeService>();
```
> **为什么放在 `AddCloudSyncServer()` 而非 `AddApplicationServices()`**
> - `AddApplicationServices()` 被 MAUI 端调用,MAUI 不需要也不会启动云同步探测
> - `AddCloudSyncServer()` 仅在 Host 的 `Program.cs` 中被调用,确保服务仅注册在服务端
**端点注册(`CloudSyncEndpointExtensions.MapCloudSyncEndpoints()`**
`MapCloudSyncEndpoints()` 方法内部新增端点组:
```csharp
var probe = app.MapGroup("/cloud-sync").WithTags("CloudSync - Setup");
probe.MapPost("/probe", ProbeAsync).AllowAnonymous();
```
端点实现:
```csharp
private static async Task<IResult> ProbeAsync(
ProbeRequest request,
CloudProbeService probeService,
CancellationToken cancellationToken)
{
if (request == null || string.IsNullOrWhiteSpace(request.TargetUrl))
{
return CloudApiErrors.BadRequest("TargetUrl is required.");
}
var result = await probeService.ProbeAsync(request.TargetUrl, cancellationToken);
return Results.Json(result);
}
```
**前端修改** `src/Hua.Todo.Web/src/components/CloudSyncSettingsDialog.vue`
删除 `probeReachability()` 函数(原 L235-L280),`saveServerUrl()` 中改为:
```typescript
// 旧:前端直接 fetch
// probeResult.value = await probeReachability(serverUrlSaved.value);
// 新:调用 Host 的探测接口
const response = await cloudClient.post<ProbeResponse>('/cloud-sync/probe', {
targetUrl: serverUrlSaved.value,
});
probeResult.value = {
type: response.data.type as ProbeType,
title: response.data.title,
desc: response.data.description,
};
```
#### 3.2 修改:登录统一走 Host
**核心思路**Host 自身就是云同步服务方,`/auth/login` 已实现完整认证。前端只需改为走 Host 同源请求。
**前端 `cloudClient.ts` 改造**`src/Hua.Todo.Web/src/api/cloudClient.ts`
移除对外部 `serverUrl` 的依赖:
```typescript
// 改造前(L36-L39):
// const { serverUrl } = CloudSyncStorage.loadSettings();
// if (serverUrl) { config.baseURL = serverUrl; }
// 改造后:不设 baseURL,axios 使用浏览器默认同源
// cloudClient 请求直接打到当前 Host/auth/* , /tasks/* , /cloud-sync/*
```
> **同源适配说明**
> - **Host 模式**Vite dev):Vue 运行在 :5174`/api` 走 proxy 到 Host(:5173),云同步端点 `/auth/*` 需配 Vite proxy 或直接用 Host 地址
> - **MAUI 模式**WebView):Vue 部署在 MAUI 嵌入服务器同源,不需要额外代理
> - 具体方案:在 Vite dev 时增加 proxy 规则将 `/auth`、`/tasks`、`/cloud-sync` 也 proxy 到 Host;或在 cloudClient 中按环境设置 baseURL
**前端 `CloudSyncSettingsDialog.vue` 的 `login()` 无需修改**
`cloudSyncApi.login()` 调用 `cloudClient.post('/auth/login', ...)`,自动打到 Host。
**Host 端 `CloudAuthService.LoginAsync` 已有完整实现**
- 创建 `UserSessionEntity` 写入 DBsession 管理在 Host
- 返回 `LoginResponse``AccessToken` = DB SessionId
- Session 验证由 `SessionAuthenticationHandler` 查询 DB 完成
#### 3.3 Vite 代理配置补充
Vite dev 模式下,除了已有的 `/api` 代理,需**补充云同步端点的代理规则**(`vite.config.ts`):
```typescript
server: {
port: 5174,
proxy: {
'/api': {
target: 'http://localhost:5173',
changeOrigin: true,
secure: false,
},
// 新增:云同步端点代理到 Host
'/auth': {
target: 'http://localhost:5173',
changeOrigin: true,
secure: false,
},
'/tasks': {
target: 'http://localhost:5173',
changeOrigin: true,
secure: false,
},
'/sync': {
target: 'http://localhost:5173',
changeOrigin: true,
secure: false,
},
'/security': {
target: 'http://localhost:5173',
changeOrigin: true,
secure: false,
},
'/cloud-sync': {
target: 'http://localhost:5173',
changeOrigin: true,
secure: false,
},
},
},
```
> **MAUI 模式无需此配置**MAUI 使用 `vite build --mode maui`,静态部署到嵌入服务器同源。
#### 3.4 `serverUrl` 字段的语义变化
| 维度 | 改造前 | 改造后 |
|---|---|---|
| **用途** | 云同步 API 的 base URL(前端直连) | 仅用于"服务端探测"的目标地址 |
| **存储位置** | `localStorage` | 不变(探测时使用) |
| **影响登录/Todo 拉取** | 是(前端用它做 baseURL) | 否(走 Host 自身端点) |
### 4. 需要变更的文件清单
| 层 | 文件 | 变更类型 | 说明 |
|---|---|---|---|
| Application | `Services/CloudProbeService.cs` | **新增** | 服务端探测逻辑,注册于 `AddCloudSyncServer()` |
| Application | `Models/AdminDtos.cs`(或新 `Models/ProbeDtos.cs` | **新增 DTO** | `ProbeRequest` / `ProbeResponse`(纯数据合约) |
| Application | `CloudSyncEndpointExtensions.cs` | **修改** | 注册 `POST /cloud-sync/probe` |
| Application | `CloudSyncServiceCollectionExtensions.cs` | **修改** | `AddCloudSyncServer()` 内注册 `CloudProbeService` + `HttpClient` |
| Host | `Program.cs` | **无需修改** | `MapCloudSyncEndpoints()` 自动包含新端点 |
| Web(Vue) | `vite.config.ts` | **修改** | 补充云同步端点的 dev proxy |
| Web(Vue) | `api/cloudClient.ts` | **修改** | 移除外部 `serverUrl` baseURL |
| Web(Vue) | `api/cloudSync.ts` | **修改** | 新增 `probeServerUrl()` 方法 |
| Web(Vue) | `components/CloudSyncSettingsDialog.vue` | **修改** | 删除 `probeReachability()`,改用 API 调用 |
| 层 | 文件 | 变更 | 说明 |
|---|---|---|---|
| MAUI | `MauiProgram.cs` | **无需修改** | 不注册 `AddCloudSyncServer()` |
| MAUI | `EmbeddedWebServerService.cs` | **无需修改** | 不映射 `MapCloudSyncEndpoints()` |
### 5. 端点路由变更汇总
| 路由 | 方法 | 变更 | 可用环境 |
|---|---|---|---|
| `/cloud-sync/probe` | POST | **新增** | HostVite dev proxy/ MAUI(同源请求到 Host |
| `/auth/login` | POST | 无变更 | 同上 |
| `/auth/step-up` | POST | 无变更 | 同上 |
| `/tasks/` | GET | 无变更 | 同上 |
| `/security/policy` | GET | 无变更 | 同上 |
### 6. 服务端/客户端职责边界总结
```
┌──────────────────────────────────────────────────────────────┐
│ Hua.Todo.Application (共享层) │
│ │
│ AddApplicationServices() AddCloudSyncServer() │
│ ├─ TodoDbContext ├─ CloudAuthService │
│ ├─ TaskRepository ├─ CloudAdminService │
│ ├─ TaskService ├─ CloudTaskSyncService │
│ ├─ DynamicApi ├─ SecurityPolicyService │
│ └─ DTOs (Models/*) ├─ CloudProbeService (新) │
│ ↑ 纯数据合约,两端安全 ├─ Authentication/Policy │
│ └─ MapCloudSyncEndpoints │
│ │
│ Host 注册: AddApplicationServices + AddCloudSyncServer │
│ MAUI 注册: AddApplicationServices only │
└──────────────────────────────────────────────────────────────┘
```
### 7. 安全考量
| 风险点 | 缓解措施 |
|---|---|
| SSRF(探测端点攻击内网) | `CloudProbeService` 限制目标 URL 必须是 HTTP/HTTPS 公网地址,禁止探测 localhost/内网 IP |
| 探测请求被滥用 | 加频率限制(如每分钟 3 次),仅允许同源请求 |
| MAUI 端不暴露云同步端点 | `AddCloudSyncServer()` 仅在 Host 调用,MAUI 无法访问云同步端点 |