Update: 将子项目从 submodule 转为完整内容

- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
This commit is contained in:
freedak
2026-07-04 19:20:46 +08:00
parent 54d6465fa7
commit f7a720204a
3360 changed files with 802660 additions and 3 deletions
@@ -0,0 +1,104 @@
# 终端三项改进 设计文档(自动标题 + 回退 Shell + 退出清理)
> 状态:已与用户确认,进入实现。语言随上下文用中文;代码标识符用英文。
## 目标
1. **自动标题**:新建终端会话的侧边栏标题太固定,改为在用户首次交互后,自动总结为与工作内容相关的标题。
2. **回退 Shell**claude/codex 终端会话「失去激活态」后出现乱码卡死、无法输入、进程生死不明的情况,改为可靠回退到一个干净的 shell(而非卡死页面),并允许用户持续 Ctrl+C 脱身。
3. **退出清理**:整个 App 退出时清理所有终端会话(删除全部会话行 + 滚屏),不残留到下次启动。
## 现状架构(与改动相关)
- 每个会话 = `portable-pty` 子进程(Windows ConPTY)。`TerminalService.live: DashMap<i64, Arc<PtyHandle>>` 管理活进程,`next_epoch` 区分代际,`on_exit` 回调按 epoch 守卫。子进程退出后 `last_status='exited'`,PTY 被销毁,**无自动重启**。
- `relaunch(id)`service.rs:837= kill 旧 PTY → 同一 id、新 epoch 用**原命令**重启 → `clear_scrollback``update_status('running')``emit_updated`
- 标题就是 `terminal_sessions.name``default_name(command, backend)`service.rs:989)机械决定:backend 标签(`Claude`/`Codex`> `Shell``$SHELL` 哨兵)> 原始命令。重命名链路 `update_meta → repo.update_meta → emit terminal.updated → 前端 onUpdated → 侧边栏重渲染` **已全链路工作**
- 终端会话**无 conversation/provider/model 关联**DB 行、`CreateTerminalParams`、API DTO、IPC、UI 均无相关列)。
- 全部输入经 `TerminalService::input(id, data_b64)`service.rs:743)汇聚后写入 PTY`pty.rs::write` 不留记录。`input()` 在无活 handle 时返回 `NotFound`
- 生命周期事件:`LifecycleKind{TurnEnd, ToolUse, Notification, SessionStart}`lifecycle.rs)。claude 用 Stop→TurnEndpayload 含 `last_assistant_message`),codex 同有 TurnEnd。`subscribe_lifecycle(id)` 已存在;spawn_pty 内已有一个示例消费者(service.rs:515-529)。
- 既有「prompt→短文本」LLM 路径:`nomifun_ai_agent::one_shot_completion(cfg, system, messages, max_tokens)` + `resolve_provider_config(provider_repo, encryption_key, provider_id, model, workspace)` + `user_message(text)``LiveKnowledgeCompleter`knowledge_completer.rs)是「持有 provider_repo + encryption_key + workspace、`resolve_default_model()` 取第一个有效 provider/model」的拷贝模板。`provider_repo`/`encryption_key``AppServices`services.rs:173/176),`terminal_service` 在 services.rs:440-459 接线,但二者**未**传给 `TerminalService`
- 前端:`XtermView.tsx` 的 xterm 网格**从不**按 status 禁用(`onData` 始终转发输入);仅 `TerminalSendBox``isExited``last_status!=='running'`)时 disableTerminalSessionPage.tsx:408)。`term.clear()` 只清普通缓冲、**不能**退出 alt-screen;需 `term.reset()`。WebGL 上下文丢失(XtermView.tsx:89-95)是「失去激活态后乱码」的诱因之一。
- **WS 重连间隙**`httpBridge.ts` 的 WS 单例会重连(退避 1s~30s),监听器按事件名存于模块级 map 故重连后存活;但服务端只做无 replay 的 `broadcast_all`,重连 open 回调**不重新拉取 scrollback** → 断线期间的重绘帧永久丢失 = 持久乱码。
- App 生命周期钩子仅在 `apps/desktop/src/main.rs``on_window_event`CloseRequested 隐藏到托盘;Destroyed→exit(0))、tray-quit(设 `QuitFlag``app.exit(0)`)、`handle_run_event`(仅 macOS Reopen)。无任何终端清理;`TerminalService`/`PtyHandle` 无 Drop。后端运行在独立 tokio runtime 线程,`DesktopServer` 暴露 runtime Handledesktop.rs:106)但无阻塞 shutdown 方法。
---
## 功能① 自动标题
**与原始需求的偏差(已确认)**:终端无 conversation 关联,无法「取对应对话的 provider/model」。改为:用 **App 默认有效 provider/model**(首个有效 provider 的首个有效 model,复用 knowledge 自动生成模式)做 LLM 总结;**模型未配置或调用失败 → 兜底取用户输入内容前 N 字**。
### 触发与范围
- **仅一次**,首次交互时填充;**含 shell**。
- **claude/codex**:订阅生命周期,首个 `TurnEnd` 事件读 `payload.last_assistant_message`,结合已捕获的首行用户输入,调 `one_shot_completion` 总结为短标题(≤ ~30 字)。
- **shell / 非 agent / 无模型 / LLM 失败**:取首行用户输入前 N 字(默认 N=40,去除控制字符)作为标题。
- **捕获首行输入**:在 `TerminalService::input()` 内累积该会话首个输入直到首个 `\r`/`\n`,存内存。
### 一次性 + 不覆盖手动改名
- 写入前判定 `当前 name == default_name(command, backend)`;不等说明用户已改名或已自动命名过 → 跳过(自身幂等)。
- 另加内存 `titled: DashMap<i64, ()>` 防止首批按键竞态重复触发。
- **无需 migration**。
### 落库
- 统一走现有 `update_meta(id, Some(title), None)`trim/校验/持久化/emit `terminal.updated`),**前端零改动**。
### 后端接线
- 新增 `LiveTerminalTitleCompleter { provider_repo, encryption_key, workspace }`(仿 `LiveKnowledgeCompleter`),late-wire 注入 `TerminalService`(新增 `with_title_completer`)。`None` 时只走兜底截断,绝不阻塞。
- `crates/backend/nomifun-terminal/Cargo.toml``nomifun-ai-agent` 依赖。
- `services.rs:440-459` 旁注入,复用 `provider_repo.clone()` / `encryption_key` / `data_dir.clone()`
### 主要文件
- `crates/backend/nomifun-terminal/src/{service.rs, title.rs(新), Cargo.toml}`
- `crates/backend/nomifun-app/src/services.rs`
---
## 功能② 回退 Shell(修复乱码卡死 + 持续 Ctrl+C 脱身)
三个独立缺陷,一套协同修复:
1. **后端 `relaunch_as_shell(id)`**:克隆 `relaunch()` 逻辑,但用 `SHELL_SENTINEL`+`[]` 替代 `row.command`/`row.args` 来 spawn。先 tree-kill 卡住的 agent → 同一 id、新 epoch 重启干净登录 shell → 持久化 `command=$SHELL, args=[], backend=None`(这样退出/重启后续仍是 shell,且 `default_name``Shell`)→ `update_status('running')``emit_updated`(前端 `onUpdated` 自动恢复送信框)。
- 新路由 `POST /api/terminals/{id}/relaunch-shell`(独立于现有 relaunch,语义清晰)。
- IPC`ipcBridge.terminal.relaunchShell(id)`
2. **前端 `term.reset()`**:给 `XtermViewHandle``reset:()=>term.reset()`(退出 alt-screen、复位模式、清屏)。在「回退 Shell」与重连重放时调用。
3. **常驻「回退 Shell」入口(不受 isExited 限制)**:会话头部加按钮;点击 = `relaunchShell` + `term.reset()`。即便会话仍 `running` 卡死也能脱身。
4. **持续 Ctrl+C 升级**`XtermView` 检测短时间内连续 N 次 Ctrl+C(`\x03`,默认 3 次 / 1.5s)→ 显示提示条「再次 Ctrl+C 回退到 Shell」并在阈值后触发 `relaunchShell`。Ctrl+C 仍原样转发(不拦截单次)。
5. **WS 重连重放(乱码主因修复)**`httpBridge.ts` WS open 时若为重连,通知监听者(新增 `terminal.__reconnected` 内部事件或重连回调);`XtermView` 收到后 `term.reset()` 然后重新 `ipcBridge.terminal.get(id)` 用同一 decoder 重放当前 scrollback。
### 主要文件
- `crates/backend/nomifun-terminal/src/{service.rs, routes.rs}`
- `ui/src/renderer/pages/terminal/{XtermView.tsx, TerminalSessionPage.tsx, TerminalSendBox.tsx}`
- `ui/src/common/adapter/{ipcBridge.ts, httpBridge.ts}`
- i18n`ui/.../locales/{zh-CN,en-US}/terminal.json`
---
## 功能③ 退出清理(删除全部会话行 + 滚屏)
1. **repo**`ITerminalRepository``delete_all(&self) -> Result<u64>`SQLite 实现 `DELETE FROM terminal_sessions``terminal_scrollback` 经 FK CASCADE 自动删)。MemRepo 同步实现(供测试)。
2. **service**`TerminalService::shutdown_cleanup()`:遍历 `self.live` 逐个 `kill()`,清 `pending_spawn`,再 `repo.delete_all()`
3. **desktop**`DesktopServer::shutdown_terminals()`:把 `shutdown_cleanup()` 编排到后端 runtime 上**阻塞执行(带超时上限,如 3s)**,供 Tauri 主线程在 `app.exit(0)` 前同步调用。
4. **main.rs**:仅在 **real-quit 路径**调用:
- tray-quit handler`QuitFlag` 已置位,main.rs:697-702`app.exit(0)` 之前;
- 新增 `RunEvent::ExitRequested`/`Exit` 分支(`handle_run_event`,仅当 `QuitFlag` 置位时)。
- **绝不在 close-to-tray(隐藏窗口)路径调用**。
### 主要文件
- `crates/backend/nomifun-terminal/src/service.rs`
- `crates/backend/nomifun-db/src/repository/{terminal.rs, sqlite_terminal.rs}`
- `crates/backend/nomifun-app/src/desktop.rs`
- `apps/desktop/src/main.rs`
---
## 测试策略
- **①** `default_name` 一致性守卫的幂等(改名后不再触发);无 completer 注入时的截断兜底;注入 fake completer 时 TurnEnd→update_meta 链路;首行输入捕获。
- **②** `relaunch_as_shell` 单测:同一 id、`command` 变为 `$SHELL`、status 回 `running`、发 `terminal.updated`。前端 reset / 重连重放 / 连续 Ctrl+C 升级以手动 + 单元(纯函数)验证。
- **③** `delete_all` 删行 + scrollbackCASCADE);`shutdown_cleanup` kill 活进程后行清空;close-to-tray 不触发(QuitFlag 守卫,逻辑断言)。
## 风险与缓解
- **退出清理的破坏性**:用户已确认要删全部;严格只在 QuitFlag 真退出路径执行,close-to-tray 绝不删。阻塞清理带 3s 超时上限,避免退出卡住。
- **回退 shell 改变语义**:原 agent 退出→可重启项;改 shell 后命令被改写为 `$SHELL`。这是用户明确要的「回退到 shell」,且仅在用户主动点按/连续 Ctrl+C 触发,不做静默自动改写。
- **标题 LLM 成本/噪声**:仅首次、仅一次、`name==default` 守卫;agent 才调 LLMshell 走零成本截断。
- **Windows 进程组**`kill()` 在 Windows 无进程组 SIGKILL,孙进程可能残留——沿用既有 `kill()` 能力,不在本次扩大范围。