Files
agent-desirecore/skills/app-install-manager/SKILL.md
Yige ee0bc7b044 fix: 绑定目录获取与生命周期收据 (#10)
* fix(apps): 绑定目录快照与最终执行

* fix(apps): 结算目录执行前失败

* fix(services): 拒绝不完整目录响应

* fix(services): 按生命周期收据精确卸载

* fix(apps): 采用服务端生命周期收据

* fix(apps): 绑定目录操作防重放

* fix(apps): 原子切换重装收据
2026-08-31 04:00:50 -04:00

385 lines
28 KiB
Markdown
Raw 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.
---
name: 应用安装管理
description: 经目录快照或已安装生命周期收据 resolver 授权后安装/卸载/启停 docker-app 与 mcp/http-api 服务docker-app解析 manifest/installGuide → 跑 docker compose → 健康校验 → 回写安装状态mcp 服务:使用 resolver 返回的 install/connection → 注册到 Agent → 连接验证 → 回写状态。Use when 用户要求"安装 Dify/n8n 等应用"、"安装某 MCP 服务"、"卸载某应用/服务"、"启动/停止/重启某应用",或安装/卸载请求以"请安装/卸载 {名称} 到/从 {设备}"形式到达。
version: "1.3.0"
type: procedural
risk_level: high
status: enabled
disable-model-invocation: true
tags: [installation, docker, mcp, registry, app-management]
metadata:
author: desirecore
updated_at: "2026-08-31"
---
# app-install-manager 技能
## L0一句话摘要
应用/服务生命周期执行者:把目录里的 docker-app 与 mcp/http-api 服务真正装起来/卸下去,并把真实结果回写到安装记录,让"应用与服务"界面反映真实状态。
## L1概述与使用场景
DesireCore 的安装是**委派式**的——界面发送自然语言说明加机器 envelope并乐观记下一条中间态
(首装 `installing` / 重装 `reinstalling` / 卸载 `uninstalling`**真正的执行与终态回写由本技能
(你)完成**。后端会监听安装记录文件:在状态变为 `installed` 后自动派生 docker-app 暴露的服务,
在中间态保留既有派生,仅在终态(`failed`/`uninstalled`/条目移除)清理派生。
覆盖两类目标:
- **docker-app**Dify / n8n / RagFlow 等,走 docker compose 部署。
- **mcp / http-api 服务**MCP 服务器(按 registry 的 `install` 字段安装 + 注册到 Agent与 HTTP API 服务(仅登记安装记录,无自动化部署动作)。
使用场景:
- 用户说"安装 Dify""把 n8n 装到本机""安装某 MCP 服务"
- 用户说"卸载 RagFlow""卸载某 MCP 服务""停止/启动/重启 Open WebUI"
- 收到形如"请安装 {name} 到{device}""请卸载 {name}"的指令
## L2详细规范SOP
### 关键路径与数据
- 当前目录快照解析:`POST http://127.0.0.1:<agent-service-port>/api/registry/acquisitions/resolve`
- 机器消息解析与响应复核:`<本技能目录>/scripts/registry-catalog-acquisition.mjs`
- 安装记录:`<DesireCore根目录>/config/installed-entries.json`
`<根目录>` 为你的 AgentFS 根,生产为 `~/.desirecore`,开发隔离为 `~/.desirecore-dev`,以自我感知里的实际根目录为准)
- agent-service API`http://127.0.0.1:<agent-service-port>`端口见自我感知mcp 服务安装/注册用)
### 执行前协议:精确机器消息 → ownership / resolver强制
任何 install/reinstall/uninstall 在环境探测、Docker 查询、包安装或其它执行类 `bash` 前,都必须
先完成本节。自然语言只用于面向用户的说明,**不得**从自然语言猜测 `kind``sourceId`
`entryId`、deviceId、snapshot、install 或 connection。
1. 从**当前这条**生命周期指令中提取唯一一行:
`RegistryCatalogAcquisition=<单行 JSON>`。App/Service envelope 都必须带
`operation:{action:"install"|"reinstall"|"uninstall",deviceId,operationId}`operationId 必须是小写
UUID并且只代表这一次 lifecycle 意图。先把指令全文交给
`parse-locator`,只取得 `sourceId+entryId+operation.deviceId` 的账本定位符;再用
`parse-message` 校验完整 envelope
```bash
node "<本技能目录>/scripts/registry-catalog-acquisition.mjs" parse-locator
node "<本技能目录>/scripts/registry-catalog-acquisition.mjs" parse-message
```
完整解析结果为 `{request,locator}`App resolver request 必须包含 operation。解析器会拒绝缺失、
重复、畸形机器行、额外字段、可变 commit、来源/snapshot 不一致和 App 夹带 install/connection。
不允许回看旧消息或从显示名、同 ID 本地条目、自然语言补字段。若 locator 已解析但完整解析
失败,先按“执行前停止的强制结算”处理;真正缺失/JSON 畸形且没有 locator 的手工指令不猜写
安装账本,只报告错误并停止。
2. 对 App 三种 operation 及 Service install/reinstall把解析结果的 `request` 原样作为 body 调 resolver
```yaml
tool: HttpRequest
parameters:
url: http://127.0.0.1:<agent-service-port>/api/registry/acquisitions/resolve
method: POST
body: <parse-message 输出的 request>
```
install/reinstall 由服务端重验 current canonicaluninstall 由服务端按 operation.deviceId 精确读取
installed-entry 的不可变 lifecycle receipt并用 receipt 对应的 Git object 返回旧版本定义。它不因
当前 catalog listing-only、stale 或下架而阻断,但无 receipt、receipt/snapshot 不一致、来源或设备
不匹配都失败关闭。
Service uninstall 不调用 current catalog resolver它只按 locator 命中 exact installed receipt随后
由精确 DELETE 端点使用 receipt.runtimeServerId 删除目录删除、listing-only 或 stale 不得卡住卸载。
3. 只有 HTTP 200 且响应通过同一脚本的 `evaluate-response` 复核后才可继续。复核
输入是 `{ "expected": <request>, "status": <HTTP状态>, "body": <响应JSON> }`。响应的
kind/sourceId/entryId/snapshot、manifest.id/type 必须一致App 还必须取得非空 installGuide。
响应还必须包含服务端生成并校验的完整 `catalogReceipt` candidate。`allowed:true` 才是本次执行
定义App 只使用其 manifest/installGuide/catalogReceipt忽略任何 install/connection。uninstall 的
manifest/installGuide/receipt 必须来自服务端 receipt 解析结果,不能改读当前目录。
4. 服务端有界拒绝码必须逐字保留:
`registry_acquisition_invalid_request`、`registry_acquisition_not_found`、
`registry_acquisition_snapshot_stale`、`registry_acquisition_blocked`、
`registry_acquisition_client_upgrade_required`、`registry_acquisition_config_mismatch`、
`registry_acquisition_install_guide_unavailable`、`registry_acquisition_receipt_missing`、
`registry_acquisition_ownership_mismatch`;同时保留通过有界校验的 reasons。任何非 200、
连接失败、非 JSON 或身份/快照/manifest 不一致都禁止执行命令,并先完成强制结算。
5. **禁止 fallback**:不得读取任何本地 Registry 条目、改用同 ID 其它来源或沿用旧 resolver 响应。
resolver/ownership 只决定本次读取与精确归属,不新增安装账本,也不替代 Human Gate、健康校验、
回滚和终态收据。
安装记录条目结构(写回时必须完整保留全部字段):
```json
{
"entryId": "dify",
"sourceId": "registry:official",
"type": "docker-app | mcp | http-api",
"deviceId": "<设备ID>",
"deviceName": "<设备名>",
"version": "<版本>",
"installedAt": 1730000000000,
"installedBy": "agent",
"status": "installing | reinstalling | installed | uninstalling | failed | uninstalled",
"conversationId": "<对话ID>",
"messageId": "<消息ID>",
"operationId": "<本次操作UUID>",
"catalogReceipt": {
"schemaVersion": 1,
"kind": "app | service",
"catalogSourceId": "registry:official",
"entryId": "dify",
"catalogCommit": "<40/64位commit>",
"catalogPath": "entries/dify",
"releaseVersion": "<安装版本>",
"lifecycle": {
"manifest": {
"id": "dify",
"name": "Dify",
"type": "docker-app | mcp",
"version": "<安装版本>",
"description": "<最多4096字符>"
},
"installGuide": "<仅App服务端返回且最多64KiB>"
},
"runtimeServerId": "<仅MCP注册成功后由服务端签发>"
}
}
```
`catalogReceipt` 是同一 installed-entry 内的最小不可变生命周期收据不是第二账本。UI 意图只带
snapshot 基线install/reinstall 成功时必须原样采用 resolver 返回的完整 candidate禁止 Skill 根据
snapshot、manifest 或 installGuide 自行构造/扩展 lifecycle。Service 注册成功后只允许在该 candidate
上补服务端返回的 runtimeServerId。
重装时主记录的 `catalogReceipt` 和 version 仍是旧的已安装事实;新目标只存在同一条记录的
`pendingCatalogReceipt`base snapshot无 lifecycle/runtime。Skill 不写 PUT也不把 pending 当成
最终 receipt。成功时用 resolver 的新 candidate + 新 releaseVersion 原子切换;失败/取消只用同一
operationId PATCH `installed`,省略 receipt/version让服务端保留旧事实并清除 pending。
**状态语义表(六枚举)**——中间态由界面乐观写入、终态由你回写:
| status | 谁写入 | 你的退出动作 |
|--------|--------|-------------|
| `installing` | 界面首装乐观态 | 成功→`installed`;失败→`failed` |
| `reinstalling` | 界面重装乐观态(此前已 `installed` | 成功→`installed`;失败但**旧版本仍在运行**→回写 `installed` 并说明重装失败;失败且应用已不可用→`failed` |
| `uninstalling` | 界面卸载乐观态 | 成功→`uninstalled`(或移除条目);**失败→回写 `installed`** 并向用户说明失败原因 |
| `installed` / `failed` / `uninstalled` | 你回写的终态 | — |
**后端派生规则(务必理解)**:只在 `installed` 派生 docker-app 的服务;在中间态(`installing`/`reinstalling`/`uninstalling`**保留**既有派生;仅在终态(`failed`/`uninstalled`/条目移除)**清理**派生。因此:
- 重装/卸载期间派生服务不会被误删(旧容器还在跑时目录不抖动);
- 卸载或重装失败时你回写 `installed`,派生会**无缝恢复**(从未被删);
- `failed` 语义是"应用当前不可用"——**只有确认容器已不能用才写 `failed`**,否则一律回 `installed`。
### 执行前停止的强制结算
UI 在发消息前已经写入中间态。locator 成功后完整解析失败、resolver 拒绝/异常、Human Gate
取消、环境检查失败,或任何尚未执行安装/卸载命令的退出,都必须按下面的单账本流程结算:
1. `GET /api/installed-entries` 取得当前实例记录;不得直读其它实例或构造新记录。
2. 把 `{stage,locator,entries}` 交给脚本的 `plan-settlement`。stage 只能是
`parse|resolver|human_gate|pre_execution`。它严格匹配 sourceId+entryId+deviceId 和与 operation
对应的中间态与 operationId同 ID 双来源、跨设备或旧消息重放都不会串写,零条/多条都返回
settlement:null。
3. settlement 非空时按“回写安装记录的统一方式”PATCH 它给出的精确三元组和 status
`installing → failed``reinstalling → installed``uninstalling → installed`,并原样携带
settlement.operationId。结算前后都不得执行
Docker、包管理或安装脚本。settlement:null 时报告有界原因并停止,不按数组顺序、名称或设备猜测。
4. Human Gate 取消也是正常停止而非悬空:首装回 `failed`,重装/卸载恢复 `installed`。resolver
错误码与 reasons 在完成结算后原样说明。
### 回写安装记录的统一方式端点优先404 降级)
**所有「回写安装记录」都用这一方式**——不要再直接 file-write 改 `installed-entries.json`(除非端点不可用时降级)。这样 installed-entries 成为「你校验后回写的事实」,界面以它为准。
**首选PATCH 端点**(结构化 + 原子写 + 自动广播刷新前端):
```yaml
tool: HttpRequest
parameters:
url: http://127.0.0.1:<agent-service-port>/api/installed-entries/<entryId>/<deviceId>
method: PATCH
body:
sourceId: <locator 中的精确 sourceId>
operationId: <locator.operation.operationId>
status: installed # 六枚举之一
# version: "<新版本>" # 可选,重装升级时更新版本号
```
- `200` → 回写成功,前端自动刷新,**无需**再手动改文件。
- `400` → status/sourceId 非法,检查取值。
- `404 entry_not_found` → 该条中间态记录不存在(界面未写/已被清理)。**不要**重试或伪造记录;跳过并一句话提示用户重发指令即可。
- receipt PATCH 的 `catalog_receipt_*` / `catalog_operation_mismatch` /
`catalog_status_transition_invalid` / `invalid_operation_id` 400/409
表示候选非法、ownership/CAS/runtime/lifecycle/operation 不一致;
不得删字段、重建 receipt 或换 snapshot 重试。App 停止并按失败语义结算MCP 先回滚刚注册的
runtimeServerId再结算。
- **连接失败 / 路由 404旧客户端无此端点** → catalog lifecycle 回写不得降级 file-write否则会
绕过 operationId 和 receipt CAS。停止并报告客户端不兼容。
**降级 file-write**(仅无 catalog envelope 的历史普通操作可用;本节 catalog intent 绝无此降级):读
`installed-entries.json` → 按 `sourceId`+`entryId`+`deviceId` 精确定位那条中间态记录 → **只改
`status`(保留 `installedAt` 等其余所有字段与其它条目)** → 写回整个文件(界面也写此文件,
勿覆盖丢失)。来源缺失或存在多条候选时失败关闭,不得按同 ID 猜测。
下文各流程的「回写安装记录」一律指这套统一方式,只标注目标 `status`。
### docker-app 安装流程
1. **解析机器消息并调用 resolver**严格执行“执行前协议”。action 与 deviceId 只认 operation
`entryId`、`type`、版本、端口、exposes 和安装步骤只认复核后的服务端结果。manifest.type 不是
`docker-app` 时改走下方服务流程;身份或类型不一致立即停止。
2. **读取本次授权定义**:从 resolver 返回的 manifest 读取 `install.requirements`docker/内存/
磁盘/ports与 exposes只把 resolver 返回的 installGuide 作为部署步骤与验证地址。不得从
AgentFS 或其它目录补读/覆盖同 ID 定义。
3. **环境校验**`bash``docker version` / `docker compose version` 确认 docker 就绪;用
`manifest.install.requirements.ports` 检查端口占用(`lsof -i :<port>` 或 `docker ps`);检查磁盘
空间。任一不满足→先按 `pre_execution` 结算再停下,向用户说明并给出修复建议。
4. **高风险确认**:安装/卸载会改动本机容器,属高风险。执行前用一句话向用户确认(应用名 +
目标设备 + 端口)。用户取消则按 `human_gate` 结算后中止。
5. **执行**`bash`,严格按 resolver 返回的 installGuide
- docker-compose 类:在应用工作目录 `docker compose up -d`docker 类:`docker run ...`。
- 失败立即捕获输出,进入"失败处理"。
6. **健康校验(先校验后回写,强制)**:按 installGuide 的验证地址或 manifest.exposes 的 `http://localhost:<port><path>``bash` 用 `curl` 轮询(最多 ~2 分钟)确认服务可达。**只有这步通过才算安装成功**——不要仅凭 `docker compose up -d` 无报错就回写 `installed`。
7. **回写安装记录****本技能的核心职责**,按上方「回写安装记录的统一方式」):
- 健康校验通过 → 用 `build-receipt-patch` 把 resolver 原样 candidate 与 `status: installed` 组成
CAS-safe PATCH并携带 locator.operationId 和 candidate.releaseVersion不得本地重建 receipt。
未通过/失败 → 首装 `status: failed`;重装失败但旧版本仍在运行 → 只 PATCH
`{sourceId,operationId,status:installed}`,不得附 receipt/version/pending见状态语义表
- 成功后无需手动派生服务——后端文件 watcher 检测到 `installed` 后自动派生;重装期间派生始终保留。
8. **回报用户**:一句话总结结果 + 访问地址(成功)或失败原因 + 排查建议(失败)。
### docker-app 卸载流程
1. 完整解析 App envelope 并调用 resolver。resolver 必须用 operation.deviceId 精确命中
sourceId+entryId+deviceId 的 `uninstalling` 记录,再按该记录的不可变 lifecycle receipt 读取旧 Git
object**不得以当前 catalog eligibility 重新授权卸载**。同 ID 其它来源、其它设备、无 receipt
legacy 记录或 receipt/snapshot 不一致都拒绝。
2. 只使用 resolver 从 receipt 返回并通过脚本复核的 manifest/installGuide 作为卸载定义;不得读取
当前 Registry 同 ID 条目补命令。resolver 拒绝时先结算回 `installed` 并提示手工管理/升级,不能
发明 compose 目录或容器名。取得定义后再做 Human Gate取消即回 `installed`。
3. `bash`:只按 receipt 绑定的 installGuide 执行卸载(如 `docker compose down -v`),按需清理
卷/镜像。
4. 回写安装记录(按「回写安装记录的统一方式」):
- **成功**(容器确已停止/删除)→ PATCH `status: uninstalled`。后端 watcher 据此清理派生服务与 per-service Skill。
- **失败**(容器未能停止/删除,应用仍在运行)→ PATCH `status: installed`,向用户说明卸载失败原因。**切勿**留在 `uninstalling`(界面卸载按钮会禁用,用户被卡住直至 stale 超时)。
5. 回报用户。
### mcp / http-api 服务安装流程
**mcp 服务**manifest.type=`mcp`,条目含 `install` 与 `connection` 字段):
1. **解析机器消息并调用 resolver + 确认**:严格执行“执行前协议”,以复核后的复合身份确定
`entryId`,以 operation.deviceId 确定目标设备mcp 通常装到本机。resolver 失败时在任何安装命令前
停止;成功后再做高风险确认。界面已乐观写 `installing`/`reinstalling`。
2. **使用本次授权定义**:只用 resolver 响应的 manifest/install/connection`install` 含 method、
packageName、command、args、postInstall`connection` 含 transport/command/args/url/headers
不读取或合并本地同 ID 条目。
3. **执行安装**(优先走 API逐条跑 `postInstall` 命令 + 可选连接测试):
```yaml
tool: HttpRequest
parameters:
url: http://127.0.0.1:<agent-service-port>/api/mcp/install
method: POST
body:
install: <manifest.install 原样>
connection: <manifest.connection 原样> # 传入则安装后自动测连接
catalogAcquisition: <parse-message 输出的 request 原样>
```
返回 `data.steps`(每条命令 exitCode/stdout/stderr与 `data.connectionTest`。任一命令失败→`success:false`,进入"失败处理"。无 API 可用时用 `bash` 逐条跑 `postInstall`。
4. **注册到 Agent**(让 MCP 工具下轮可用):
```yaml
tool: HttpRequest
parameters:
url: http://127.0.0.1:<agent-service-port>/api/agents/desirecore/mcp-servers
method: POST
body:
serverId: <entryId>
config: <manifest.connection 原样>
catalogAcquisition: <parse-message 输出的 request 原样>
```
端点锁内 read-modify-write 写入 agent.json 的 `mcp_servers`——**勿手工编辑 agent.json**(绕锁会丢
并发更新。catalog 响应的 `data.runtimeServerId` 是服务端签发的复合 ownership key不得用
entryId、显示名或调用方值代替。
5. **连接校验(先校验后回写,强制)**:看第 3 步返回的 `connectionTest.success`,或单独 `POST /api/mcp/test-connection`body `{connection}`)确认能连通、能列出工具。**只有校验通过才算安装成功**——不要仅凭 postInstall 命令退出码 0 就回写 `installed`(装了包不等于连得上)。
6. **回写安装记录**(按「回写安装记录的统一方式」):连接校验通过后,用脚本
`build-receipt-patch` 原样接收 resolver 的 catalogReceipt candidate只在其上补服务端 add 返回的
runtimeServerId并与 locator.operationId、`status: installed` 在同一次精确 PATCH 中写回原
installed-entryPATCH version 取 candidate.releaseVersion禁止从
request snapshot/manifest 自建或替换 lifecycle。缺 candidate/runtimeServerId、candidate 被篡改或
receipt CAS 回写失败时不得宣布成功,先按精确 runtimeServerId 回滚刚注册的 MCP再按失败语义
结算。连接校验失败 → 首装 `failed`;重装失败但旧配置仍可用 → 只带同一 operationId 恢复
`installed`,省略 receipt/version/pending。
7. **回报用户**:总结安装结果 + 发现的工具数(成功)或失败原因摘要(失败)。
**http-api 服务**manifest.type=`http-api`,无 `install` 字段、界面也无自动化安装动作):按「回写安装记录的统一方式」维护回写(`installing`→`installed`、`uninstalling`→`uninstalled`/失败回 `installed`),明确告知用户该类服务无本地部署步骤、只是登记可达性。
### mcp / http-api 服务卸载流程
1. 完整解析机器消息,但不调用 current catalog resolver。用脚本 `resolve-service-uninstall` 对 locator、
request.snapshot 和 `GET /api/installed-entries` 结果做完整复核:必须精确命中
sourceId+entryId+deviceId 的唯一 `uninstalling` 记录receipt.kind 必须为 service
catalogReceipt.runtimeServerId 与 snapshot 必须有效。无 receipt/kind/runtimeServerId、身份/快照不符、
零条或多条都先结算回 `installed` 并停止。随后做高风险确认,取消同样结算回 `installed`。
2. **执行卸载**mcp从 Agent 移除 MCP server 配置:
```yaml
tool: HttpRequest
parameters:
url: http://127.0.0.1:<agent-service-port>/api/agents/desirecore/mcp-servers/<entryId>?sourceId=<sourceId>&deviceId=<deviceId>&operationId=<operationId>
method: DELETE
```
URL 参数先用脚本 `build-service-delete` 从 locator 生成operationId 必须原样来自 locator。服务端
从精确 installed-entry receipt 的 runtimeServerId 删除对应
配置;不得按裸 entryId 删除,
也不得因为当前目录删除、listing-only 或 snapshot stale 改走 resolver。
端点幂等。如安装时全局装了包,按需 `bash` 卸载可选多为无害保留。http-api 服务无需执行
动作,直接进第 3 步。
- **旧客户端降级**:该 DELETE 端点是较新客户端才有的能力。若返回 **404 / Not Found / 路由不存在**,说明当前客户端版本尚未包含 mcp 卸载端点——**不要**当作卸载成功。此时回写安装记录为 `installed`(保持"仍在用"),并一句话告知用户"当前客户端版本不支持 mcp 服务卸载,请升级客户端后重试"。切勿手工编辑 agent.json 绕过(绕锁会丢并发更新)。
3. **回写安装记录**(按「回写安装记录的统一方式」):成功 → 用同一 operationId PATCH
`status: uninstalled`**失败(含 DELETE mcp-servers 端点 404 降级)→ 用同一 operationId PATCH
`status: installed`** 并说明原因(勿留在 `uninstalling`)。
4. 回报用户。
### 启动 / 停止 / 重启
收到"启动/停止/重启 {应用}"docker-app不走 catalog acquisition envelope它只能以资源管理面
已经精确选中的 sourceId+entryId+deviceId 和实例级生命周期定义执行。缺精确实例或定义时停止,
不得用名称、本地 Registry 同 ID 条目或当前 catalog 猜容器。完成高风险确认后才用 `bash` 执行
该实例定义允许的 start/stop/restart这类运行态切换不改变 installed-entry 的 install 状态。
### 失败处理
| 场景 | 处理 |
|------|------|
| 机器行缺失/畸形/重复 | 无 locator 时不猜写;有 locator 时先强制结算。都禁止从自然语言或本地目录补全 |
| resolver 400/404/409/连接失败 | 保留真实 `registry_acquisition_*` 与有界 reasons先强制结算再停止禁止 Docker/bash/包管理和同 ID fallback |
| resolver 200 但身份/快照/manifest/指南不一致 | 视为不可信响应,先强制结算,不执行任何生命周期副作用 |
| resolver 200 缺失/篡改 catalogReceipt 或 App lifecycle guide 超过 64 KiB | 拒绝执行;禁止本地补建/截断 lifecycle先强制结算 |
| receipt PATCH CAS/runtime/lifecycle 冲突 | 禁止修改候选绕过MCP 回滚注册后结算App 按失败语义结算 |
| operationId 缺失/畸形或旧消息重放 | 失败关闭;不得用当前 installed-entry 的新 operationId 替换旧消息,禁止执行或结算新意图 |
| Human Gate 取消 | 首装回 `failed`,重装/卸载回 `installed`,不得遗留中间态 |
| docker 未运行 | 提示用户启动 Docker首装回 `failed`,重装回 `installed` |
| 端口被占用 | 列出占用进程,建议换端口或停占用,征求用户意见 |
| compose 启动失败 | `docker compose logs` 取错误,回写 `failed`,附日志摘要 |
| 健康校验超时 | 提示"可能仍在启动",给出查看日志的命令;如确认失败回写 `failed` |
| **卸载失败**(容器/配置未能移除,应用仍可用) | 回写 **`installed`**(不是 `failed`、不留 `uninstalling`),向用户说明卸载失败原因与排查建议 |
| **重装失败** | 旧版本仍在运行→回写 **`installed`** 并说明重装失败;旧版本已损坏不可用→`failed` |
| **mcp postInstall 失败** | 回写 `failed`,附失败命令的 stderr/exitCode 摘要;不注册到 Agent |
| **mcp 卸载端点 404**(旧客户端无 DELETE mcp-servers 能力) | 回写 `installed`(不是 `uninstalled`),告知用户升级客户端后重试 mcp 卸载;不手工改 agent.json |
### 边界与安全
- 新安装只处理当前机器消息与 resolver 共同确认的应用/服务;卸载只处理精确 installed ownership。
- `sourceId+entryId+snapshot+operation.deviceId+operation.operationId` 是本次边界;显示名、历史消息、本地同 ID 条目、
installed-entries 和调用方提供的 install/connection 都不能扩大它。
- App resolver 必须先于 Docker 探测、包管理和所有执行类 `bash`;非 200 或复核失败没有 fallback
并必须先结算中间态。uninstall 不受当前 catalog listing/stale 阻断,但只能使用精确 ownership 与
lifecycle receipt 解析出的旧版本定义。
- 所有破坏性 docker 操作与 Agent 配置写入前必须有用户确认risk_level: high
- 回写状态优先用 PATCH 端点(自带 status 枚举校验与原子写);仅端点不可用时降级 file-write此时须自行保证结构合法status 仅限六枚举值),否则界面加载会过滤掉脏条目。
- **先校验后回写**docker-app 必须健康校验通过、mcp 必须连接校验通过,才回写 `installed`——installed-entries 是「你校验过的事实」,不是「执行过命令」。
- **中间态是过渡态,你必须回写终态或(卸载/重装失败时)回 `installed`**——绝不把记录停在 `installing`/`reinstalling`/`uninstalling`,否则界面对应操作按钮会禁用、用户被卡住。
## 与其他技能/系统的协作
- **后端 installed-entries watcher**:消费你回写的 status只在 `installed` 派生 docker-app 服务、中间态保留派生、终态清理,无需你手动调派生接口。
- **Registry acquisition resolver**`POST /api/registry/acquisitions/resolve`install/reinstall 复核当前
canonical snapshotApp uninstall 按精确 ownership + lifecycle receipt 解析旧 Git object只授权
本次读取,不记录安装事实)。
- **installed-entries 回写端点**`PATCH /api/installed-entries/:entryId/:deviceId`body 携带 sourceId结构化回写状态原子写 + 自动广播刷新前端;旧客户端 404 时才降级 file-write
- **agent-service mcp API**`POST /api/mcp/install`(执行 postInstall + 连接测试)、`POST /api/agents/desirecore/mcp-servers`(注册)、`DELETE /api/agents/desirecore/mcp-servers/:serverId`(卸载)、`POST /api/mcp/test-connection`(验证)。
- **task-management**:长安装可登记为任务跟踪进度。
- **service-health**:派生出的服务由后端周期探活,你无需自行维护其健康。