<!-- 文档分类: IMPL-RPT-010 | 阶段: 实现 | 原路径: docs/修复记录/网关适配器可用性严格化-本地安装与登录-2026-06-10.md -->
# 网关 IDE 适配器「可用」状态严格化修复（必须本地安装且登录）

修复日期：2026-06-10 · 分支：feat/workflow-provider-export-and-mcp

---

## 概述

网关状态面板把 **Cursor / Windsurf / Warp** 显示为「可用」，但这些 IDE 本机根本
没有安装。「可用」必须如实反映本地真实状态，否则用户会被误导去选用一个发不出请求
的通道。

本次将适配器可用性判定收紧为严格语义：

```
available = 本地已安装(installedLocally) && 本地真实登录(hasGenuineLocalLogin)
```

导入 / 账号池（Nirvana 换号缓存、pool）凭证单独存在**不再**算「可用」，仅经显式
开关 `KHY_GATEWAY_ALLOW_IMPORTED_CREDENTIALS=1` 恢复旧行为。Warp 必须有真实
Warp 登录态——剪贴板工具（xclip/pbcopy）只是传输手段，不再作为可用性门槛。

> **路由 / 请求路径不变**：pool / 导入 token 仍可驱动实际请求；本次只把「能发请求」
> 与「本地是否可用」解耦，仅改可用性 / 显示判定。

---

## 一、现象

在未安装任何 IDE 的机器上打开网关状态面板：

- Cursor / Windsurf / Warp 显示「可用」——明显错误。

用户反馈原话：

> cursor,windsurf,warp 这些 ide 都没有安装为什么显示可用这是不对的

---

## 二、根因（已读源码确认）

### 根因 1：可用性只看「有没有 token」，从不 AND 安装检测

- **cursor** `detect()`：`_available = !!(_token && hasTokenShape)`，且保留 `pool:`
  来源 token 使其长期可用。
- **windsurf** `detect()`：算出了 `_installPath` 却没用，`_available = !!_token`。
- **trae** `detect()`：token 可能来自 Nirvana 换号缓存（`TRAE_STORAGE_PATHS` /
  `NIRVANA_TRAE_CACHE_PATHS` 混合了原生与导入路径）。
- **kiro** `detect()`：后台 pool token 直接翻转 `_available = true`，无任何开关。
- **warp** `detect()`：`= clipboardRelayAdapter.detect()`——只要有 xclip/pbcopy
  就算可用，与 Warp 是否安装 / 登录毫无关系。

### 根因 2：模块级 `_available` 缓存陈旧

只有 `destroy()` / `refreshAdapters()` / 强制 `detect(true)` 才会重置；
credentialWatcher 只往 pool 写 token，从不重置适配器缓存。
`aiGateway.getStatus()` 同步聚合各适配器 `getStatus()`，直接返回陈旧 `_available`。

---

## 三、修复架构

### 单一真源分类器 — `_ideTokenMixin.js`

新增并导出 4 个助手，作为所有适配器共享的唯一判定来源：

- `allowImportedCredentials()`：**唯一**读取 `KHY_GATEWAY_ALLOW_IMPORTED_CREDENTIALS`
  的地方（默认 OFF=严格），适配器不得直接读 `process.env`。
- `classifyTokenSource(token)` → `'local' | 'pool' | 'nirvana'`，复用各
  `readXToken()` 已写入的 `source` / `path` 字段：
  - `pool`：`source` 以 `pool:` 开头；
  - `nirvana`：`source` ∈ {`nirvana-cache`,`nirvana`} 或 `path` 命中 Nirvana 路径；
  - `local`：其余（原生 IDE 存储）。
- `isNativeLoginToken(token)`：`isLikelyCredentialToken(accessToken) && 来源为 local`。
- `countsTowardAvailability(token)`：
  `isNativeLoginToken(token) || (allowImportedCredentials() && isLikelyCredentialToken(accessToken))`。

> **设计要点**：从 IDE **自身原生存储**读到的 token 即「真实登录」凭据——它本身
> 已是「安装 + 登录」的权威证明，故 `isNativeLoginToken` 命中时短路为可用，无需再
> 二次 `ideDetector` 校验。导入 / 池凭证则必须额外满足开关。

### 四个 IDE 适配器套用同一模式（cursor / windsurf / trae / kiro）

```js
function _computeAvailable(token) {
  if (isNativeLoginToken(token)) return true;          // 原生登录=权威证明
  return _installed() && countsTowardAvailability(token); // 否则须本地安装+开关
}
```

- `_installed()` → `ideDetector.findInstallation(name) || findDataPath(name)`。
- 收紧「纯 pool 来源即可用」分支；**保留** `_token` 赋值供 `generate()` / 请求使用。
- `detectAsync()` 仍可用 pool token 路由，但其上报的**可用布尔值**必须过
  `countsTowardAvailability`。

### Warp 重写 — `warpAdapter.js`

- 新增只读、不写 pool 的 `accountPool.detectWarpLocalLogin()`，内部复用
  `importWarpCandidates()`（已读 `WARP_STORAGE_PATHS` 并提取
  `warpAuth.accessToken` / email / expiresAt），返回 `{installed, hasLogin, email}`，
  保持 `WARP_STORAGE_PATHS` 单一真源。
- `detect()`：`_available = installed && hasLogin`；剪贴板中继降级为
  `transport: 'clipboard-relay'` 标注，不再作为门槛。
- `getStatus()` 三态文案：已登录（剪贴板可用 / 不可用）、已安装未登录、未安装。

### 陈旧缓存修复（同步 getStatus 路径）

每个适配器 `getStatus()` 改为 `detect(true)` **同步重算**——
`findInstallation||findDataPath`（廉价 `fs.existsSync`）+ 原生 `readXToken()`。
安装 / 登录消失即刻降级为不可用，绕开 60s `_detectCache`。

### trae 文案诚实化

trae `getStatus()` 的「已安装 / 未安装」判定改用严格 `ideDetector` 信号
（`_traeStrictInstalled()`），不再被 Nirvana 换号缓存污染的 `detectInstallation()`
误报「已安装」。

---

## 四、文件改动

| 文件 | 改动 |
|---|---|
| `src/services/gateway/adapters/_ideTokenMixin.js` | 新增并导出 4 个共享助手（开关 + 来源分类 + 可用性判定），单一真源 |
| `src/services/gateway/adapters/cursorAdapter.js` | `detect()` / `getStatus()` 套用严格模式 + 同步重算 |
| `src/services/gateway/adapters/windsurfAdapter.js` | 启用 `_installPath`，`detect()` 套用 `_computeAvailable` |
| `src/services/gateway/adapters/traeAdapter.js` | `detect()` 区分原生 / Nirvana；`getStatus()` 文案改用严格安装信号 |
| `src/services/gateway/adapters/kiroAdapter.js` | 后台 pool 可用性加开关 + 安装门控；`getStatus()` 同步重算 |
| `src/services/gateway/adapters/warpAdapter.js` | 重写 detect / getStatus（真实安装+登录，剪贴板仅传输） |
| `src/services/accountPool.js` | 新增只读导出 `detectWarpLocalLogin()` |

---

## 五、opt-in 开关与回滚

- `KHY_GATEWAY_ALLOW_IMPORTED_CREDENTIALS`：默认 **OFF = 严格**。
- 仅经 `allowImportedCredentials()` 读取（单一真源）。设为 `1` / `true` / `on` / `yes`
  → pool / 导入 token 计入可用，**恢复旧行为**（即回滚路径）。
- 与现有 `{PREFIX}_TOKEN_PRIORITY`（仅排序路由候选）相互独立。

---

## 六、零回归保证

- 行为变更受开关控制，默认严格；路由 / generate 路径不动，pool token 仍用于请求。
- 新检测均为同步 `fs.existsSync` + 适配器已有读取，性能开销可忽略。

---

## 七、验证

仓库根 `/home/kodehu03/Khy-OS` 运行（**勿用** `npx jest` / `jest` bin，rtk 钩子会报
`Exec format error`）：

```
node node_modules/jest-cli/bin/jest.js --rootDir services/backend <pattern>
```

### 新增测试套件（39 用例全绿）

- `tests/gateway/strictAvailability.test.js`（24）——共享分类器全矩阵：开关解析、
  local/pool/nirvana 分类、原生登录、导入凭证开关开 / 关。
- `tests/gateway/warpAdapter.strictAvailability.test.js`（5）——未装 / 已装未登录 /
  已登录 / 剪贴板缺失 / 登出后 getStatus 同步降级。
- `tests/gateway/cursorAdapter.strictAvailability.test.js`（4，代表四个 IDE 适配器）
  ——已装+原生登录可用、干净机不可用、已装未登录不可用、原生 token 为安装权威证明。

### 手动 E2E（本机无任何 IDE）

```
warp     => available: false | 未检测到 Warp 安装
cursor   => available: false | 未检测到 Cursor 安装
windsurf => available: false | 未检测到 Windsurf 安装或 token
trae     => available: false | 未检测到 Trae 安装
kiro     => available: false | 未检测到 Kiro token …
```

——用户原始投诉（cursor / windsurf / warp 显示可用）已解决。

### 回归

相关网关套件全过。`tests/gateway` 目录中 5 个失败套件（7 用例：
`gatewayDebugPrompt` / `gatewayStatus.endpointVisibility` /
`gatewayStatus.preferredAdapterHint` / `gateway/aiGateway.exports` /
`gatewayManage.portDrift.integration`）经 `git stash` 验证在 HEAD 上**同样失败**，
属预存问题，与本次改动无关。**本次零回归。**

---

## 八、备注

- 测试陷阱：`CURSOR_STORAGE_PATHS` / `CURSOR_DB_PATHS` 等 env 覆盖在模块加载期
  并入路径表，故 cursor 适配器测试须**在 require 之前**设置；并 `jest.doMock`
  `ideDetector` 以确定性控制安装态。
- 可选未做（§5b）：在 `credentialWatcherService` 检测到本地登录文件增删后调用
  `aiGateway.invalidateDetectCache(provider)` 更快反映真实登录——严格默认下 pool
  写入不改可用性，仅为体验优化、非正确性必需。
