feat: harden install and resumable OAuth login
CI / test (macos-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s

This commit is contained in:
nac
2026-08-28 07:01:20 +00:00
parent 6b5b3a413a
commit 9f3d3ad5fc
19 changed files with 1721 additions and 472 deletions
+92 -38
View File
@@ -1,16 +1,19 @@
# 安装 NocoBase CRM MCP
# 安装或更新 NocoBase CRM MCP
这是一份供终端型 AI Agent 执行的通用安装协议,不依赖任何特定 Agent 或 MCP 客户端。用户只需要提供本链接、确认本机变更,并在浏览器中登录自己的 NocoBase 账号完成授权。
这是一份供终端型 AI Agent 执行的通用安装与更新协议,不依赖任何特定 Agent 或 MCP 客户端。用户每次都可以提供同一个链接:Agent 必须先识别是首次安装、更新还是已经为最新版,再执行对应流程。只有确实需要认证时,用户才在浏览器中登录自己的 NocoBase 账号完成授权。
如果你是安装 Agent,请直接执行流程,不要让用户逐条尝试命令。只有在确认系统工具变更或等待用户在 NocoBase 网页登录授权时才中断用户。
统一入口:`https://git.justconn.top/gitea-admin/nocobase-mcp/raw/branch/main/INSTALL.md`。执行 Agent 必须按下文将其固定到任务开始时的完整 Commit SHA。
如果你是执行 Agent,请直接完成流程,不要让用户逐条尝试命令。只有在确认系统工具变更或确实等待用户在 NocoBase 网页登录授权时才中断用户;不得仅因更新而要求用户重新登录。
## 完成标准
- 从 Gitea `main` 解析安装开始时的最新完整 Commit SHA,并将本次安装的文档、源码和构建全部固定到该 Commit;
- 从 Gitea `main` 解析任务开始时的最新完整 Commit SHA,并将本次任务的文档、源码和构建全部固定到该 Commit;
- 只连接固定的 NocoBase API `https://justconn.top/api`;
- MCP 在用户本机以 stdio 运行,默认只读;
- 通过 NocoBase 原生 OAuth 2.1 认证当前用户,权限仍跟随本人的业务角色;
- 当前 MCP 客户端已注册 `nocobase-crm`;
- 首次安装、更新或已是最新的判断有明确依据,更新保留原有 CRM 配置、个人凭据和只读/写入模式;
- `crm_health` 返回 `ok=true`。
## 安全规则
@@ -20,15 +23,28 @@
- OAuth Token 不得进入聊天、命令参数、环境变量、CRM 配置、剪贴板日志或 MCP 客户端配置。
- 不使用 `sudo`,不执行 `curl | sh` 或 `wget | sh`。
- 不静默安装 Git、Node.js、包管理器或其他系统工具。
- 解析 `main` 后只使用带完整 Commit SHA 的不可变 URL;一次安装中不得再次读取移动中的 `main` 文件。
- 解析 `main` 后只使用带完整 Commit SHA 的不可变 URL;一次任务中不得再次读取移动中的 `main` 文件。
- 最新 Commit 无法下载、检查、测试或构建时必须停止,不得静默退回旧 Release、旧 Commit 或其他来源。
- 修改 MCP 客户端配置前展示目标文件与新增项,创建备份,并结构化合并;不得覆盖其他 server。
- 更新不得读取、复制、覆盖或删除个人凭据,不得覆盖现有 CRM `config.json`,也不得擅自改变 `--enable-writes` 模式。
- 默认不启用写入、原始数据、后台服务或网络监听。
- 不询问或接受其他 NocoBase URL,也不允许通过参数、配置或凭证文件覆盖固定 API 地址。
## 安装流程
## 安装或更新流程
### 1. 解析并固定最新 Commit
### 1. 判断首次安装、更新或已有最新版
先检查当前 MCP 客户端配置中的 `nocobase-crm` 条目、其启动命令和参数,以及推荐用户级安装目录中是否已有可执行文件。若存在 `<install-prefix>/install-receipt.json`,可以读取其中不含秘密的安装记录;只有 `schemaVersion=1`、仓库 URL 完全匹配、`commitSha` 为 40 位十六进制、`tarballSha256` 为 64 位十六进制且其余字段类型正确时才视为有效。无效或缺失的记录不能作为跳过构建的依据。不得打开、解析或输出凭证文件内容,只能在必要时判断凭证文件是否存在。
按以下规则确定初始状态:
- **首次安装:** 没有可用的 `nocobase-crm` 配置和已安装命令。默认只读,除非用户在本次任务中明确要求启用固定项目写入。
- **更新:** 已有配置和可执行命令。沿用其安装前缀、CRM 配置路径及 `--enable-writes` 状态,除非用户明确要求变更;个人凭据始终原地保留。
- **修复旧安装:** 只找到配置、命令或安装目录的一部分。说明检测到的不一致,然后按更新方式修复缺失部分,不删除仍可用的配置或凭据。
记录检测结果、现有安装目录、当前版本、原读写模式,以及安装记录中的旧完整 Commit SHA(若存在)。旧版本没有安装记录不是错误,第一次更新时正常重建并补写记录。不得仅凭 npm 包版本判断 Commit 是否相同,因为多个 Commit 可以使用同一包版本。
### 2. 解析并固定最新 Commit
确定当前 Agent/MCP 客户端及其配置路径,以及普通用户应用数据目录。NocoBase API 已固定为 `https://justconn.top/api`,不需要向用户询问地址。
@@ -38,7 +54,7 @@
- 分支:`main`
- 分支 API:`https://git.justconn.top/api/v1/repos/gitea-admin/nocobase-mcp/branches/main`
通过当前用户已经授权的 Gitea API/连接读取分支 API,只解析一次 `commit.id`,并验证它是 40 位十六进制完整 SHA。记录为 `<commit-sha>` 后,本次安装即使发现 `main` 又有新提交,也继续完成这个已固定快照;下一次安装才重新解析最新提交。
通过当前用户已经授权的 Gitea API/连接读取分支 API,只解析一次 `commit.id`,并验证它是 40 位十六进制完整 SHA。记录为 `<commit-sha>` 后,本次任务即使发现 `main` 又有新提交,也继续完成这个已固定快照;下一次安装或更新才重新解析最新提交。
立即从以下不可变地址重新读取安装协议,后续不再依赖最初的 `main` 页面内容:
@@ -48,13 +64,15 @@ https://git.justconn.top/gitea-admin/nocobase-mcp/raw/commit/<commit-sha>/INSTAL
仓库访问受限时必须使用当前用户已有的授权连接;无权访问时停止,不得索取或接收用户在聊天中发送的 Gitea 访问令牌。
### 2. 一次性说明本机变更
若来源为本仓库的有效安装记录中的旧 SHA 等于 `<commit-sha>`,还必须确认已安装命令的 `--version` 与记录一致、`--help` 正常、MCP 配置指向存在的入口。全部一致时将操作判定为**已有最新版**:跳过源码下载、构建和 npm 安装,直接进入 MCP 配置核对与 `crm_health` 验收。任何一项不一致都按更新/修复继续,不能因为包版本看起来相同而跳过。
检查 `node --version` 和 `npm --version`。要求 Node.js `>=20.11`。
### 3. 一次性说明本机变更
向用户一次性说明本次判定为首次安装、更新、修复还是已有最新版,以及目标 Commit、安装目录、将保留的内容和预计变更。检查 `node --version` 和 `npm --version`;需要构建或安装时要求 Node.js `>=20.11`。
如果 Node.js 缺失或版本过低,向用户说明需要安装或升级 Node.js、官方来源、目标位置和影响,取得明确同意后再使用操作系统的正规安装方式。不能因为 npm 不存在就直接尝试全局安装。还需确认 npm 能读取锁定依赖;不得改用未锁定的依赖版本。
正常安装只会留下:
首次安装或更新只会留下:
- 应用数据目录中的 npm 包;
- 不含凭据的 CRM `config.json`;
@@ -63,7 +81,9 @@ https://git.justconn.top/gitea-admin/nocobase-mcp/raw/commit/<commit-sha>/INSTAL
源码快照、构建期 `node_modules` 和本地 tarball 只放在新建的临时目录,安装完成后不作为持久文件保留。不会安装 Git、Python、uv、`nb CLI`、后台服务或本机监听端口。
### 3. 下载、验证并构建固定 Commit
更新或修复前备份 MCP 客户端配置和非秘密 CRM `config.json`。凭据位于包目录之外,只原地保留,不为更新另行读取或复制。若判定为已有最新版且配置无需修改,不要为了形式上的更新创建多余备份或重写文件。
### 4. 下载、验证并构建固定 Commit
在新建的临时目录中下载 `<commit-sha>` 的源码归档,不下载 Release 资产,也不从移动的 `main` URL 下载源码。Windows 使用 `.zip`,macOS/Linux 使用 `.tar.gz`;两种归档都必须包含同一个已解析的完整 Commit SHA:
@@ -72,11 +92,18 @@ Windows: https://git.justconn.top/gitea-admin/nocobase-mcp/archive/<commit-s
macOS/Linux: https://git.justconn.top/gitea-admin/nocobase-mcp/archive/<commit-sha>.tar.gz
```
不要让用户选择版本,也不要因为操作系统不同而改用 Release。每次新的安装任务都重新解析当时 `main` 的最新 Commit,随后只下载并使用该 SHA。
不要让用户选择版本,也不要因为操作系统不同而改用 Release。每次新的安装或更新任务都重新解析当时 `main` 的最新 Commit,随后只下载并使用该 SHA。只有已按第 2 步确认安装记录与最新 SHA 相同且现有入口完整时,才跳过本节。
下载后记录归档 SHA-256。解包前拒绝绝对路径、`..` 路径穿越或逃出临时目录的链接;源码根目录必须包含 `package.json`、`package-lock.json`、`install-manifest.json`、`INSTALL.md` 和 `src/`。
确认归档中的 `install-manifest.json` 声明 `distribution.branch` 为 `main`、`distribution.strategy` 为 `resolve-latest-commit-then-pin`。随后只在这个临时源码目录运行:
安全解包实现必须在写入文件前完成兼容性预检,不要把解包工具的 API 差异误报为源码构建失败,也不要为解包安装或升级 Python:
- Python 不是安装前置条件。若 Agent 选择使用本机已有的 Python `tarfile`,不得无条件调用仅较新 Python 才支持的 `TarFile.extractall(..., filter="data")`;必须先用 `inspect.signature` 检查 `filter` 参数是否存在。
- 支持 `filter` 时仍需使用 `data` 过滤器。旧版 Python 不支持时,必须在解包前验证完整成员列表,拒绝绝对路径、盘符或 UNC 路径、任何 `..` 分段、重复目标、符号链接、硬链接、设备文件和 FIFO;随后只逐项创建目录,并通过 `extractfile()` 将普通文件复制到已确认位于新建临时根目录内的目标。不得直接调用不带过滤器的 `extractall()`。
- Windows `.zip` 也必须先验证所有条目并拒绝上述路径、链接和特殊文件,再逐项解包。解包目录必须是本次任务新建且仅当前用户可写的临时目录。
- 这种预先选择兼容实现仍是在处理同一个已校验哈希的归档和同一个固定 Commit,不属于版本回退,不需要重新解析 `main`、重新下载归档或中断用户。只有当前环境没有任何可安全验证并解包归档的现有工具时才停止。
确认归档中的 `install-manifest.json` 声明 `distribution.branch` 为 `main`、`distribution.strategy` 为 `resolve-latest-commit-then-pin`、`installation.supportsInPlaceUpdate=true`、`installation.sameCommitBuildSkip=true`,并且 `installation.installReceipt` 声明 `schemaVersion=1`、`containsSecrets=false`。随后只在这个临时源码目录运行:
```bash
npm ci
@@ -88,7 +115,7 @@ npm pack --pack-destination <temporary-output-directory>
任何一步失败都停止并报告 `<commit-sha>` 与失败步骤,不得改装旧 Release。记录本地生成 npm tarball 的 SHA-256;安装结束时只报告哈希,不发送或上传 tarball。
选择用户级安装前缀,例如:
首次安装时选择用户级安装前缀;更新或修复时沿用检测到的原前缀。例如:
- Windows:`%LOCALAPPDATA%\nocobase-crm-mcp\app`
- macOS:`~/Library/Application Support/nocobase-crm-mcp/app`
@@ -100,47 +127,64 @@ npm pack --pack-destination <temporary-output-directory>
npm install --prefix <install-prefix> --omit=dev <locally-built-commit.tgz>
```
`config.json` 和个人凭据必须位于应用数据目录而不是 npm 包目录内;不得在安装前清空 `<install-prefix>`,也不得通过删除整个应用数据目录来“更新”。如果安装命令失败,报告失败并保留可恢复信息,不得删除原配置或凭据。
定位安装后的命令:POSIX 为 `<install-prefix>/node_modules/.bin/nocobase-crm-mcp`,Windows 为相同目录下的 `nocobase-crm-mcp.cmd`。运行 `--version` 和 `--help`,确认版本等于源码 `package.json`,并且帮助中包含本 Commit 安装协议使用的 `setup --no-browser` 选项;不一致时停止,不得继续注册旧二进制。
### 4. 在登录前注册 MCP
### 5. 注册或核对 MCP
先运行:
首次安装、更新或修复后运行;已有最新版时也可用它核对当前配置:
```bash
<installed-command> client-config --json
<installed-command> client-config [--config <existing-config-path>] [--enable-writes] --json
```
首次安装可省略 `--config` 使用默认位置,并默认省略 `--enable-writes`。更新或修复必须把原 MCP 条目中的 CRM 配置路径传给 `--config`,并保持原 `--enable-writes` 状态;只有用户本次明确要求改变时才增删该参数。
该命令会在需要时从包内模板准备默认 `config.json`,不要求凭据,也不会发起 OAuth。读取返回的 `clientConfig`:
1. 展示客户端配置路径和新增的 `nocobase-crm` 条目;
2. 创建可恢复备份;
3. 结构化合并该 server,保留所有其他配置;
1. 展示客户端配置路径和目标 `nocobase-crm` 条目;
2. 仅在条目需要修改时创建可恢复备份;
3. 仅当条目有变化时结构化合并该 server,保留所有其他配置;
4. 重载或重启 MCP 客户端,并在客户端要求时让用户信任该连接器。
没有凭据时 stdio MCP 仍应正常启动并响应 `tools/list`。此时业务工具会返回结构化的 `nocobase_auth_failed`,而不是导致整个 MCP 进程退出。
### 5. 通过 MCP 把登录链接交给用户
### 6. 先验收现有登录,必要时再把链接交给用户
注册完成后调用:
注册或重载完成后,**先调用 `crm_health`**。若返回 `ok=true`,说明现有登录仍然有效,直接完成验收;不得为了首次安装或更新流程的形式完整而调用登录工具、生成新链接或要求用户再次授权。
只有 `crm_health` 明确返回 `nocobase_auth_failed` 时才进入登录:首次安装且没有可用凭据时调用 `nocobase_login({})`;更新/修复中已有凭据被 NocoBase 拒绝时调用 `nocobase_login({"force": true})`。如果 `crm_health` 返回 ACL、配置、网络或超时错误,处理对应错误,不能用重新登录掩盖它。
首次安装且没有可用凭据时调用:
```text
nocobase_login({})
```
现有凭据已被 NocoBase 拒绝时改为调用:
```text
nocobase_login({"force": true})
```
该工具通过 NocoBase OAuth 发现接口动态注册本机公共客户端,请求 `api offline_access`,并立即返回:
- `login_url`:只指向 `https://justconn.top` 的完整授权链接;
- `verification_uri_complete`:与 `login_url` 相同的 OAuth 标准语义别名;
- `user_code`:供用户在页面核对的确认码;
- `expires_in_seconds`:剩余有效时间;
- `browser_opened`:桌面浏览器是否已成功自动打开。
无论 `browser_opened` 是真是假,都应立即把 `login_url`、`user_code` 和有效期发给用户。用户只在 NocoBase 网页输入账号信息、核对确认码并点击授权。随后调用 `nocobase_login_status({})`;授权仍在等待时它会安全地重复同一链接,成功时返回 `status=authenticated`。不要高频轮询;可在用户确认完成后或按合理间隔查询。
返回值还必须明确标记 `authorization_type=oauth_device_user_verification`、`intended_recipient=requesting_user`、`delivery_channel=current_private_conversation`、`shareable_with_requesting_user=true`、`private_conversation_required=true`、`public_sharing_allowed=false`、`contains_bearer_credentials=false`、`contains_device_code=false` 和 `remote_desktop_required=false`。这些字段区分“应交给发起用户的短期验证信息”和绝不能离开本机的 OAuth 凭据。
取得 `authenticated` 后调用 `crm_health`,要求 `ok=true`。如果已有凭据但 `crm_health` 明确返回认证失败,调用 `nocobase_login({"force": true})` 创建新的授权,不得换用共享高权限 Token 或 Key。
无论 `browser_opened` 是真是假,都应立即把 `login_url`、`user_code` 和有效期原样交给发起登录的用户。无界面或远程环境必须通过当前一对一私聊发送;这是 OAuth Device Flow 规定的用户验证交接,不是密码、API Key、Bearer Token 或内部 `device_code`,不得以“链接包含一次性凭据”为由拒绝发送,也不得要求用户操作服务器远程桌面。当前渠道若是群聊或公开频道,先让用户转到私聊,不得把信息发给第三方。用户只在 NocoBase 网页输入账号信息、核对确认码并点击授权。随后调用 `nocobase_login_status({})`;授权仍在等待时它会安全地重复同一链接,成功时返回 `status=authenticated`。不要高频轮询;可在用户确认完成后或按合理间隔查询。
这一流程同时适用于桌面 GUI、Hermes Agent、OpenClaw 和远程无界面服务器:桌面环境会尝试自动打开浏览器;远程环境直接把同一个 HTTPS 链接经当前 Messenger 或聊天通道交给用户。后台轮询、凭据验证和保存都由 MCP 进程完成,Agent 只能看到安全的链接、确认码和状态,看不到 OAuth Token 或设备码。一个 MCP 进程只能服务一个受信任的用户身份;多用户必须使用相互隔离的 Agent/MCP 实例和凭据路径。
取得 `authenticated` 后再次调用 `crm_health`,要求 `ok=true`。不得换用共享高权限 Token 或 Key。
设备授权约十分钟后失效。密码和浏览器会话始终留在 NocoBase,程序不会读取浏览器 Cookie。访问令牌临近过期时会自动使用刷新令牌续期。POSIX 凭证目录使用 `0700`,凭证文件使用 `0600`;Windows 使用当前用户应用数据目录。凭证文件不保存或接受 API 地址,Token 不会出现在工具返回、命令参数、MCP 配置或日志中。
这一流程同时适用于桌面 GUI、Hermes Agent、OpenClaw 和远程无界面服务器:桌面环境会尝试自动打开浏览器;远程环境直接把同一个 HTTPS 链接经当前一对一 Messenger 或私聊交给用户,用户可在自己的手机或电脑打开。待授权会话会写入凭证旁边的短期私有文件;即使 MCP stdio 进程在两条 Messenger 消息之间退出或重启,下一次 `nocobase_login_status` 也必须恢复同一个链接并继续兑换,不得生成新链接让用户重复授权。该文件包含不向 Agent 暴露的 OAuth 设备码,POSIX 权限必须为 `0600`,且在成功、取消或过期后立即删除。正式 Token 的验证和保存仍由 MCP 完成;Agent 只能看到应交给发起用户的链接、确认码和状态。一个 MCP 凭据路径只能服务一个受信任的用户身份;多用户必须使用相互隔离的 Agent/MCP 实例和凭据路径。
设备授权约十分钟后失效。密码和浏览器会话始终留在 NocoBase,程序不会读取浏览器 Cookie。访问令牌临近过期时会自动使用刷新令牌续期。POSIX 凭证目录使用 `0700`,正式凭证文件和短期待授权文件都使用 `0600`;Windows 使用当前用户应用数据目录。凭证文件不保存或接受 API 地址,Token 和设备码不会出现在工具返回、命令参数、环境变量、MCP 配置或日志中。
如果某个 MCP 客户端在登录前完全无法加载自定义连接器,才使用 CLI 备用入口:
@@ -148,7 +192,7 @@ nocobase_login({})
<installed-command> setup --no-browser --json
```
本机桌面终端也可省略 `--no-browser` 以尝试自动打开网页。CLI 会先验证或准备配置,再开始 OAuth,避免用户完成网页授权后才因配置路径不存在而丢失结果。Agent 必须持续读取该进程的输出、把链接和确认码发给用户,并让进程继续轮询。
本机桌面终端也可省略 `--no-browser` 以尝试自动打开网页。CLI 会先验证或准备配置,再开始 OAuth,避免用户完成网页授权后才因配置路径不存在而丢失结果。使用 `--json` 时,CLI 会在 stderr 输出 `event=authorization_required` 的结构化事件和上述交付标记。Agent 必须持续读取该进程的输出、把链接和确认码原样发到当前私聊,并让进程继续轮询;不得关闭进程或要求远程桌面。
只有在 OAuth 暂时不可用、管理员明确要求且用户拥有个人 Authorize Key 时,才使用:
@@ -158,7 +202,23 @@ nocobase_login({})
该备用模式必须在用户可直接控制的安全 TTY 中隐藏输入 Key;仍不得让用户把 Key 发给 Agent。
最终只报告完整 Commit SHA、源码版本、本地构建 tarball 的 SHA-256、安装目录、配置备份位置、认证方式和只读/写入模式,不显示任何 Token 或 Key。
在 `crm_health` 成功后,首次安装、更新或修复必须以原子替换方式写入 `<install-prefix>/install-receipt.json`,POSIX 权限使用 `0600`。使用以下固定字段,把占位值替换为本次实际结果:
```json
{
"schemaVersion": 1,
"repository": "https://git.justconn.top/gitea-admin/nocobase-mcp",
"commitSha": "<40-character-commit-sha>",
"packageVersion": "<package-version>",
"tarballSha256": "<64-character-sha256>",
"installedAt": "<ISO-8601-UTC-time>",
"writesEnabled": false
}
```
`writesEnabled` 必须按最终实际模式写成 `true` 或 `false`,不能始终照抄示例。该记录不得包含 Token、Key、设备码或其他凭据。已有最新版且记录完整时不重写该文件。
最终报告本次结果是“已安装”“已更新”“已修复”还是“已是最新”,并报告旧/新完整 Commit SHA(旧 SHA 可用时)、源码版本、安装目录、配置备份位置、认证是否沿用以及只读/写入模式。只有本次实际构建了 tarball 才报告新 SHA-256;已有最新版不得谎称重新构建。任何情况下都不显示 Token、Key 或设备码。
## 可选写入
@@ -166,18 +226,12 @@ nocobase_login({})
## 故障处理
- 浏览器未打开:把 `nocobase_login` 返回的 `login_url` 和 `user_code` 发给用户手动打开;不需要重启 MCP。
- 浏览器未打开:把 `nocobase_login` 返回的 `login_url` 和 `user_code` 发到发起用户的当前私聊,让用户在自己的手机或电脑打开;不需要重启 MCP 或使用服务器远程桌面。
- 授权超时或被取消:再次调用 `nocobase_login` 生成新链接并重新核对确认码。
- `nocobase_auth_failed`:调用 `nocobase_login({"force": true})` 重新授权;不要换用共享高权限 Token 或 Key。
- `nocobase_request_failed` / `NO_PERMISSIONS`:检查用户角色、字段权限、行级权限与 `config.json` 映射,不得换用共享高权限身份。
- Node.js 缺失:报告前置条件并请求用户授权正规安装,不退回 Python/uv/nb CLI 方案。
- `main` 或 Commit 归档无权访问:报告 Gitea 权限问题,不索取聊天中的访问令牌,也不退回旧 Release。
- 解包工具兼容性:在写入前改用本节规定的安全兼容实现,继续处理同一归档;不得先调用已知不受支持的 `tarfile filter` 参数,也不得因此重新解析 Commit、安装 Python 或回退版本。
- 最新 Commit 的依赖安装、检查、测试或构建失败:报告 SHA 和原始失败步骤后停止;不得以“先装可用版本”为由降级。
- 安装过程中 `main` 更新:不影响当前任务,继续完成开始时固定的 SHA;下次安装再取得新 SHA。
## 用户可发送给 Agent 的最短指令
```text
请按这个 INSTALL.md 帮我完成安装。每次先解析当前 Gitea 仓库 main 分支的最新完整 Commit SHA,并把本次安装固定在该 Commit;不要退回旧 Release。默认只读;请先注册 MCP,再调用 nocobase_login,把登录链接和确认码发给我,让我只在 NocoBase 官方网页登录并确认授权。不要向我索要密码、Token 或 Authorize Key,也不要静默安装系统工具。完成前必须验证 crm_health:
https://git.justconn.top/gitea-admin/nocobase-mcp/raw/branch/main/INSTALL.md
```
- 任务过程中 `main` 更新:不影响当前任务,继续完成开始时固定的 SHA;下次安装或更新再取得新 SHA。