# MCP `logs` 工具：container 不存在时必须回填 build 阶段失败

**Date**: 2026-05-19
**Module**: internal-app-platform / MCP tool 设计
**Related**: [2026-05-19-tool-return-value-beats-description-for-ai-followup-script.md](2026-05-19-tool-return-value-beats-description-for-ai-followup-script.md) · PR #434

## 症状

员工部署 `itadmin-carousel`，5 分钟后访问 URL 报 502，跟 Claude 说"看下日志"，Claude 调 `logs` 返：

```
container ffoa-app-itadmin-carousel doesn't exist
```

Claude 把这话原样回给员工 → 员工卡死。真实根因（npm install 报错 / 缺 start script / Dockerfile 写错）**躺在 `internal_app_events` 表的 `app.deploy_failed` 事件里**，没人去捞。

## 三层根因

| 层 | 原因 |
|---|---|
| **表层** | logs 工具回了 container_not_found，Claude 没法继续诊断 |
| **直接** | logs 工具只跑 `docker logs <container>`，从不读 events 表 |
| **元根因** | 部署链有 **2 阶段**（build → run），但 logs 工具的心智模型是"运行中容器"单阶段。build 阶段失败 → 容器从未创建 → docker logs 永远查不到 |

## 修复（两层）

### 应用层

`mcp-tools.service.ts` `logs()` 在 `container_not_found` 分支查最近一条 `app.deploy_failed` 事件回填到 `error.details.lastDeployFailure`：

```ts
if (msg === 'container_not_found') {
  const lastFailure = await this.prisma.internalAppEvent.findFirst({
    where: { appId: app.id, eventType: 'app.deploy_failed' },
    orderBy: { createdAt: 'desc' },
    select: { errorCode: true, payload: true, createdAt: true },
  });
  return this.error(
    'container_not_found',
    lastFailure
      ? `容器 ${name} 不存在 — 最近一次部署失败（${lastFailure.createdAt.toISOString()}）。errorCode=${lastFailure.errorCode}；详见 details.lastDeployFailure`
      : `容器 ${name} 不存在（尚未首次部署 / 正在构建中）。如刚 push 不到 5 分钟，请等 5 分钟后重试`,
    {
      containerName: name,
      appStatus: app.status,
      lastDeployFailure: lastFailure ? {
        errorCode: lastFailure.errorCode,
        message: (lastFailure.payload as any)?.message ?? null,
        commitSha: (lastFailure.payload as any)?.commitSha ?? null,
        failedAt: lastFailure.createdAt.toISOString(),
      } : null,
    },
  );
}
```

`@@index([appId, createdAt(sort: Desc)])` 已存在，查询是 O(1) 索引扫描。

### 工程化保险（同一个 PR）

1. **`postDeployHint` 文案加保险**：deploy_prepare 返回值的 `postDeployHint` 现在显式告诉 Claude：
   > "若 logs 返 container_not_found 且 details.lastDeployFailure 非空，说明 build 阶段失败，把 details.lastDeployFailure.errorCode + message 告诉用户"

   ——AI 行为指令走 tool 返回值，跟 [postDeployHint 设计原则](2026-05-19-tool-return-value-beats-description-for-ai-followup-script.md) 一脉相承。

2. **07-api.md 契约文档**显式记录 container_not_found error.details 结构 + Claude 行为约定。

## 通用原则：多阶段流水线的诊断工具必须跨阶段

任何"读运行时状态"的工具如果背后是 **多阶段流水线**（build → run、queue → process → publish），**默认就要回退查"前一阶段为什么没到这一阶段"**。

判别：

- 工具的对象是 **终态产物**（容器 / 文件 / 记录）
- 但产物可能 **未到达终态**（构建失败 / 任务排队中 / 上游 reject）
- 用户看了工具输出**没法判断到底发生了什么** → 必须回填

适用场景例子（不止 logs）：

| 工具 | 终态产物 | 应回退查 |
|---|---|---|
| `logs` (本案) | running container | 最近 deploy_failed 事件 |
| 查"workflow 结果" | 完成的 workflow run | pending / failed-pre-execution 状态 |
| 查"消息送达" | 投递成功的消息 | queue 中 / dropped 原因 |
| 查"PR check 状态" | 完成的 check run | 排队中 / runner 无可用 / required check 缺失 |

## 反例（已修正）

PR #434 引入 `postDeployHint` 时已经写"如仍无法访问，调 logs 工具看日志"——**潜台词是 logs 能告诉你 build 错误**。实际并不能。文案承诺超出工具能力 = AI 撞墙 = 员工绝望。

修复后两件事一起做：tool 能力补上 + 文案精准描述（"若 container_not_found 且 lastDeployFailure ≠ null"）。

## 元规则

跟 [postDeployHint](2026-05-19-tool-return-value-beats-description-for-ai-followup-script.md) 同一套：
**结构化诊断信息塞返回值里，不靠 AI 猜测 / 不靠用户去翻 DB**。

- AI 看 description / 文档 = "能干什么"
- AI 看返回值 = "现在发生了什么 + 下一步做什么"
- 返回值里多 1 个字段 = 整个用户群得到一致体验；文档里写 1 段 = 只有读过的 AI 知道
