<!-- 文档分类: IMPL-RPT-004 | 阶段: 实现 | 原路径: docs/修复记录/tui-叙事与选择覆盖层-2026-06-01.md -->
# TUI 高级工程师叙事模式 + 交互选择覆盖层

> ⚠️ **已归档 / 被取代**：本文档描述的自研 TUI 运行时（`tuiShell.js` / `OverlayHost.js` / `TranscriptViewport.js` / `PromptFrame.js` / `FooterBar.js` / `keypressParser.js` / `alternateScreen.js` 等）已于 2026-06-05 全面迁移至官方 Ink 运行时（`backend/src/cli/tui/ink-components/`），上述文件均已删除。本文仅作历史修复记录保留，最新 TUI 行为请参阅 [tui-inquirer闪退修复-2026-06-05.md](%5BIMPL-RPT-003%5D%20tui-inquirer闪退修复-2026-06-05.md)。

**日期**: 2026-06-01
**版本**: v0.1.80
**范围**: 系统提示叙事协议、交互选择 UI、表格渲染修复、默认配置变更

---

## 1. 工作叙事协议 — 让 AI 边做边汇报

### 问题

KHY TUI 中 AI 执行多步任务时只显示静默的工具卡片，两个工具调用之间没有任何文字说明。
对比 Claude Code，后者在工具调用之间有：执行前说明、结果解读、阶段性总结、完成汇报、不确定时给出选项。

### 根因

文本渲染管线完好（text chunk 不会丢失），但系统提示中只有 3 行简略的 "Broadcast + Preview + Interject"，
AI 模型没有被指示产出叙事文本。

### 修复

**`backend/src/constants/prompts.js`** — `_coreProfile()` 替换为详细工作叙事协议：

- 工具调用前：一句话说明要做什么、为什么
- 工具结果后：一句话说明结果对任务的意义
- 步骤之间：串联上一步结果和下一步动作
- 进度检查点：连续 3+ 次工具调用后简要汇总
- 完成时：总结做了什么、改了什么、需要验证什么
- 不确定时：列出编号选项请用户选择
- 硬规则：禁止两个工具卡片背靠背出现

**`backend/src/constants/outputStyles.js`** — `senior-engineer` 风格追加 2 条叙事指令。

---

## 2. 交互选择覆盖层 (Choice Overlay)

### 新增功能

支持 AI 向用户提问并收集选择（对标 Claude Code 的 AskUserQuestion）。

### 涉及文件

| 文件 | 修改项 |
|------|--------|
| `OverlayHost.js` | 新增 `renderChoice()` — 居中卡片、单选/多选、编号快捷键 |
| `AppShell.js` | 覆盖层高度兼容 `choice` 类型（用 `options` 而非 `matches`） |
| `tuiShell.js` | choice 键盘路由 + `_openChoiceOverlay` Promise 封装 |
| `tuiShell.js` | `onControlRequest` 回调接入 `ai().chat()` |

### 键盘操作

| 按键 | 单选 | 多选 |
|------|------|------|
| ↑/↓ | 移动高亮 | 移动高亮 |
| Enter | 确认当前项 | 确认所有已勾选项 |
| Esc | 取消 (返回 null) | 取消 |
| Space | — | 切换勾选 |
| 1-9 | 直接选中并确认 | 切换对应项勾选 |

### onControlRequest 回调

`ai().chat()` 现在传入 `onControlRequest` 回调，处理两种场景：

1. **AskUserQuestion** — 遍历问题列表，每个弹出 choice 覆盖层，收集答案后返回 `{ behavior: 'allow', updatedInput: { answers } }`
2. **通用工具权限** — 弹出"允许/拒绝"二选一，返回 `{ behavior: 'allow' | 'deny' }`

调用链路：`claudeAdapter` → `onControlRequest` → `_openChoiceOverlay` (Promise) → 用户键盘 → `_choiceResolve` → 返回适配器 → `normalizeControlResponse`

---

## 3. 表格渲染修复

### 现象

TUI 中 AI 返回的 Markdown 表格显示混乱 — box-drawing 边框断裂，列错位。

### 根因

`markdownRenderer` 已将表格渲染为带 `╭│├╰` box-drawing 字符的对齐行，
但 `TranscriptViewport.wrapText()` 不知道这些行是预格式化的，对它们做了二次自动换行。

### 修复

**`TranscriptViewport.js`** — markdown 渲染完成后逐行判断：

- 包含 box-drawing 字符的行 → 直接保留，不经 `wrapText`
- 普通文本行 → 继续走 `wrapText` 自动换行

检测正则：`/[╭╮╰╯│├┤┬┴┼─╌╎┌┐└┘┠┨╟╢▕▏]/`

---

## 4. 默认配置变更

### 默认登录 admin05

**`backend/bin/khy.js`** — `ensureAuthenticated()` 首次使用时自动以 `admin05` 登录：

- pip 安装后无需手动输入用户名密码，直接进入管理员模式
- 会话过期后的登录提示也更新为 `admin05 / 012003`
- 自动登录失败时回退到手动注册流程

### Thinking 默认开启

`ai.js` 中 `_thinkingEnabled = true` 已确认为默认值。

行为说明：
- `/thinking` 命令只控制**显示**，不控制模型是否思考
- 模型始终思考（gateway 始终传 `thinkingEnabled: true`，非原生模型注入 CoT 提示）
- 关闭 thinking 显示时，thinking chunk 被 `onChunk` 过滤，不进入 UI

---

## 5. 跨平台兼容性修复（对标 Claude Code）

### 问题

审计 CC 源码后发现 KHY 在 Windows/Linux 兼容方面存在多项差距：信号处理、进程杀死、Shell 派发、路径处理、终端检测等。

### 第一轮修复（基础兼容）

| 文件 | 修复 |
|------|------|
| `alternateScreen.js` | `SIGHUP` 信号在 win32 上排除 |
| `gateway.js` | `process.env.HOME` → `os.homedir()` |
| `auditLog.js` | 添加 `process.env.USERNAME` 回退（Windows 无 `USER`） |
| `cronScheduler.js` | `spawn('sh')` → 平台感知 `cmd/sh` 二选一 |
| `plugin-loader/index.js` | npm 全局路径区分 Windows (`prefix/node_modules`) / Unix (`prefix/lib/node_modules`) |
| `multiTerminalBackend.js` | tmux init 添加 win32 早退 |
| `oauthTokenStore.js` | 添加 Windows 权限注释（`mode: 0o600` 在 Windows 静默忽略） |

### 第二轮修复（CC 模式移植）

| 文件 | 修复 |
|------|------|
| `platformUtils.js` | +`isMintty()` — 检测 Git Bash/MSYS2 终端 |
| `platformUtils.js` | +`isModernWinTerminal()` — 组合检测现代 Windows 终端 |
| `alternateScreen.js` | +`getClearSequence(opts)` — legacy Windows 用 `cls`，现代终端用 VT 序列 |
| `cronScheduler.js` | `.kill('SIGTERM')` → `safeKill(proc)` 进程树杀死 |
| `windowsClipboardImg2FileService.js` | `.kill(signal)` → `safeKill(target, ...)` |
| `cliAnythingService.js` | `_which()` 委托 `searchExecutable()`；`_getVersion/_getHelp` 用 `execFileSync` 无 shell |
| `node_skeleton/backend.js` | 模板中 `which` → `where/which` 平台选择 |
| `shutdown.js` | +8s failsafe timer — `process.exit()` 卡住时 SIGKILL 自杀 |

### 已确认不需要修复的项

- **tree-kill** → `safeKill()` 已等价实现（`taskkill /T /F` + process group kill）
- **孤儿检测** → `shutdown.js:101-109` 已有 30s 轮询
- **`processAgent.js:144`** → 调用的是 `ProcessAgent.kill()` 方法，内部已用 `safeKill`
- **路径转换** → `pathCompat.js` 已有 `windowsPathToPosix/posixPathToWindows`

---

## 6. KHY 代理 WebSearch 透传修复

### 问题

Claude Code 通过 KHY 代理 (`http://127.0.0.1:9100`) 连接 Anthropic API 时，`WebSearch` 工具返回 "Did 0 searches"。

### 根因

Anthropic 的 `web_search` 是**服务端工具** (`type: "web_search_20250305"`)，不同于普通函数工具。KHY 代理管线在请求侧将其破坏：

1. `protocolConverter/anthropic.js` — `toCanonical()` 把 `{ type: "web_search_20250305" }` 映射为 `{ name, description, parameters }`，丢失 `type` 字段
2. `proxyServer.js` — `generateOptions.tools` 只传 canonical tools，无原始工具定义
3. `claudeAdapter.js` — passthrough body 用 `buildDirectToolDefs()` 重建本地工具，完全不看客户端的工具定义

### 修复

| 文件 | 修改 |
|------|------|
| `proxyServer.js` | +`rawTools: body.tools` — 保留原始工具定义（含 `type` 字段） |
| `claudeAdapter.js` | passthrough body 优先用 `options.rawTools` — 服务端工具原样到达上游 API |

响应侧无需改动 — passthrough 已直接 pipe 原始 SSE。

---

## 7. 搜索工具路由错误修复

### 问题

非 Claude 模型（如 minimax-m2.5）调用 `Search({query: "最新新闻"})` 后卡死。状态显示"完成"但回复不完整，搜索结果从未显示。

### 根因

`claudeCompat.js` 中 `TOOL_ALIASES` 将 `search` 映射到文件搜索工具（grep），而非 `webSearch`。模型的 web 搜索意图被路由到错误的工具：

- `Search({query: "最新新闻"})` → `_lookupKey` → `'search'` → `TOOL_ALIASES['search']` → **grep 工具**
- grep 在文件系统中搜不到新闻 → 返回空结果 → 模型放弃

### 修复

**`claudeCompat.js`** — `normalizeToolCall()` 新增参数感知消歧：

```javascript
if (normalizedName === 'search' && params) {
  const hasWebParams = 'query' in params || 'q' in params;
  const hasGrepParams = 'pattern' in params || 'regex' in params || 'path' in params;
  if (hasWebParams && !hasGrepParams) normalizedName = 'webSearch';
}
```

- 参数含 `query`/`q` → `webSearch`（网页搜索）
- 参数含 `pattern`/`regex`/`path` → `search`（文件搜索）

---

## 8. TUI 文字无法复制修复

### 问题

TUI 中生成的文字无法通过鼠标选择复制。

### 根因

`keypressParser.js` 启用了 `?1000h`（鼠标点击捕获），终端把所有鼠标事件（包括点击和拖选）发给 KHY 而非做原生文字选择。KHY 只处理滚轮事件（buttons 64/65），点击和拖选事件被静默丢弃。

对比 Claude Code：CC 也启用鼠标追踪，但自行实现了完整的文字选择系统（anchor/focus、word/line 选择、OSC52 剪贴板、copy-on-select）。KHY 没有这套系统。

### 修复

**`keypressParser.js`** — 移除 `?1000h`（基础点击捕获），只保留 `?1002h`（button-event，含滚轮）+ `?1006h`（SGR 格式）：

```javascript
// 改前：
this._stdout.write('\x1b[?1000h\x1b[?1002h\x1b[?1006h');
// 改后：
this._stdout.write('\x1b[?1002h\x1b[?1006h');
```

不捕获点击事件后，终端的原生文字选择/复制功能恢复正常，同时滚轮事件仍可正常工作。

---

## 涉及文件清单

| 文件 | 修改项 |
|------|--------|
| `pyproject.toml` | 版本 → 0.1.80 |
| `backend/package.json` | 版本 → 0.1.80 |
| `khy_platform/__init__.py` | 版本 → 0.1.80 |
| `backend/src/constants/prompts.js` | 工作叙事协议 |
| `backend/src/constants/outputStyles.js` | senior-engineer 风格补强 |
| `backend/src/cli/tui/components/OverlayHost.js` | renderChoice + renderHistorySearch |
| `backend/src/cli/tui/layout/AppShell.js` | 覆盖层高度兼容 |
| `backend/src/cli/tui/tuiShell.js` | choice/history-search 键盘 + onControlRequest + Ctrl+R |
| `backend/src/cli/tui/components/TranscriptViewport.js` | 表格行保护 |
| `backend/src/cli/tui/components/PromptFrame.js` | 任务进度面板 |
| `backend/src/cli/tui/components/FooterBar.js` | ￥计价显示 |
| `backend/bin/khy.js` | 默认 admin05 登录 |
| `backend/src/cli/tui/runtime/alternateScreen.js` | SIGHUP win32 排除 + getClearSequence |
| `backend/src/cli/tui/runtime/keypressParser.js` | 移除 ?1000h 恢复文字选择 |
| `backend/src/cli/handlers/gateway.js` | HOME→homedir + withTimeout |
| `backend/src/services/auditLog.js` | +USERNAME 回退 |
| `backend/src/services/cronScheduler.js` | 平台 shell + safeKill |
| `backend/src/services/windowsClipboardImg2FileService.js` | safeKill |
| `backend/src/services/multiTerminalBackend.js` | tmux win32 早退 |
| `backend/src/services/cliAnythingService.js` | 跨平台命令发现 |
| `backend/src/services/mcp/oauthTokenStore.js` | Windows 权限注释 |
| `backend/src/tools/platformUtils.js` | +isMintty +isModernWinTerminal |
| `backend/src/plugin-loader/index.js` | npm 全局路径跨平台 |
| `backend/src/data/cliAnythingTemplates/node_skeleton/backend.js` | which→where/which |
| `backend/src/bootstrap/shutdown.js` | +failsafe timer |
| `backend/src/services/gateway/proxyServer.js` | +rawTools 透传 |
| `backend/src/services/gateway/adapters/claudeAdapter.js` | passthrough 用 rawTools |
| `backend/src/services/claudeCompat.js` | Search 参数消歧 → webSearch |
