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,191 @@
# 终端三项改进 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax.
**Goal:** 终端会话自动标题、claude/codex 卡死后可靠回退 shell、App 退出时清理全部会话。
**Architecture:** 后端按功能分三块改 `nomifun-terminal`service/routes/新 title.rs)、`nomifun-db`repo delete_all)、`nomifun-app`services 接线 + desktop shutdown 桥)、`apps/desktop`(RunEvent 钩子);前端集中在 `ui/.../terminal/*` + `ipcBridge/httpBridge` + i18n。复用既有 `relaunch`/`update_meta`/`one_shot_completion`/`LiveKnowledgeCompleter`/`SHELL_SENTINEL` 模式。
**Tech Stack:** Rustaxum、sqlx 运行时查询、tokio、async-trait、dashmap)、React + TypeScriptxterm.js、Arco、bun:test)。
## Global Constraints
- 注释/文案随上下文用中文;交流用中文。提交署名 `nomifun <rika00@qq.com>`**不**加 `Co-Authored-By``git -c user.name=nomifun -c user.email=rika00@qq.com commit --author="nomifun <rika00@qq.com>" -m ...`
- 分支:`feat/terminal-improvements`(已建,设计文档已提交)。
- Rust 测试:`cargo test -p <crate> <filter>`。sqlx 用运行时查询(`sqlx::query`/`query_as`),占位符匿名 `?` 顺序 `.bind`
- 前端测试 `bun:test``cd ui && bun test <path>``import { describe, expect, test } from 'bun:test'`
- `ITerminalRepository` 是 trait + SqliteImpl + 测试 MemReposervice.rs 内)双实现 —— 新增 trait 方法必须同时改 **3 处**
- 终端会话**无 conversation 关联**;标题用 App 默认 provider/model(首个 enabled),失败/无模型兜底取用户输入前 N 字。
---
## Part C — 功能③ 退出清理(先做,最自包含)
### Task C1repo `delete_all`trait + sqlite + MemRepo
**Files:**
- Modify: `crates/backend/nomifun-db/src/repository/terminal.rs`trait 末尾加方法)
- Modify: `crates/backend/nomifun-db/src/repository/sqlite_terminal.rs`impl + 测试)
- Modify: `crates/backend/nomifun-terminal/src/service.rs`MemRepo impl
**Interfaces:**
- Produces: `async fn delete_all(&self) -> Result<u64, DbError>`(删除全部 terminal_sessions 行,scrollback 经 FK CASCADE 删除;返回删除行数)。
- [ ] **Step 1**trait 加 `async fn delete_all(&self) -> Result<u64, DbError>;`(文档注释说明 CASCADE 清 scrollback、整表删除、无 NotFound)。
- [ ] **Step 2**sqlite 实现 `DELETE FROM terminal_sessions`(无 WHERE),返回 `result.rows_affected()`
- [ ] **Step 3**MemReposervice.rs tests)实现:清空 `rows``scrollback`,返回清空前 rows 数。
- [ ] **Step 4**sqlite 测试 `delete_all_clears_rows_and_scrollback_cascade`:建 2 行 + save_scrollback → delete_all 返回 2 → list 为空 + load_scrollback None。
- [ ] **Step 5**`cargo test -p nomifun-db sqlite_terminal`
- [ ] **Step 6**:提交。
### Task C2`TerminalService::shutdown_cleanup`
**Files:** Modify `crates/backend/nomifun-terminal/src/service.rs`
**Interfaces:**
- Consumes: `repo.delete_all()`
- Produces: `pub async fn shutdown_cleanup(&self) -> Result<u64, TerminalError>`kill 全部 live PTY、清 pending_spawn、`repo.delete_all()`)。
- [ ] **Step 1**:实现:`for e in self.live.iter() { let _ = e.value().kill(); }``self.live.clear()``self.pending_spawn.clear()``let n = self.repo.delete_all().await?;` `Ok(n)`。注释强调仅 real-quit 调用。
- [ ] **Step 2**:测试 `shutdown_cleanup_kills_and_deletes_all`:建 2 个 `cat` 会话 → shutdown_cleanup → `list("u")` 为空。
- [ ] **Step 3**`cargo test -p nomifun-terminal shutdown_cleanup`
- [ ] **Step 4**:提交。
### Task C3`DesktopServer` 持有 terminal_service + 阻塞清理桥
**Files:** Modify `crates/backend/nomifun-app/src/desktop.rs`
**Interfaces:**
- Consumes: `AppServices.terminal_service: Arc<TerminalService>`pub)、`self.runtime: Handle`
- Produces: `pub fn shutdown_terminals_blocking(&self)`(在 backend runtime 上 spawn `shutdown_cleanup` 并用 std mpsc `recv_timeout(3s)` 阻塞等待;超时/错误仅 warn)。
- [ ] **Step 1**`DesktopServer` 加字段 `terminal_service: Arc<nomifun_terminal::TerminalService>``start()` 构造时 `terminal_service: services.terminal_service.clone()`(在 move 进 keep_alive 前)。
- [ ] **Step 2**:实现 `shutdown_terminals_blocking``let ts = self.terminal_service.clone(); let (tx, rx) = std::sync::mpsc::channel(); self.runtime.spawn(async move { let r = tokio::time::timeout(Duration::from_secs(3), ts.shutdown_cleanup()).await; let _ = tx.send(r); });` 然后 `match rx.recv_timeout(Duration::from_secs(4)) { ... }` 仅 log。
- [ ] **Step 3**`cargo build -p nomifun-app`
- [ ] **Step 4**:提交。
### Task C4`apps/desktop` RunEvent::ExitRequested 钩子
**Files:** Modify `apps/desktop/src/main.rs`
- [ ] **Step 1**:重写 `handle_run_event`:保留 macOS Reopen 分支;新增(全平台)`tauri::RunEvent::ExitRequested { .. } => { if let Some(server) = app.try_state::<Arc<DesktopServer>>() { server.shutdown_terminals_blocking(); } }`。注释说明:ExitRequested 只在真正退出时触发(close-to-tray 走 prevent_close 不触发),故无需 QuitFlag 守卫即安全。删除 `#[cfg(not(macos))] let _=(app,event)`(现在两平台都用到)。
- [ ] **Step 2**`cargo build -p nomifun-desktop`(或 workspace build)。
- [ ] **Step 3**:提交。
---
## Part B — 功能② 回退 Shell(后端)
### Task B1`TerminalService::relaunch_as_shell`
**Files:** Modify `crates/backend/nomifun-terminal/src/service.rs`
**Interfaces:**
- Produces: `pub async fn relaunch_as_shell(&self, id: i64) -> Result<TerminalSessionResponse, TerminalError>`
- [ ] **Step 1**:克隆 `relaunch` 逻辑,但:① 先 `repo.update_meta` 之外——改为先把行的 command/args/backend 改写为 shell:新增 repo 无关的做法是直接传 `SHELL_SENTINEL``spawn_pty`;② 持久化 shell 身份,使重启/重连后仍是 shell 且标题变 `Shell`。实现:
- 读 row`self.live.remove(&id)``kill()``self.pending_spawn.remove(&id)`
- `self.repo.update_command(id, SHELL_SENTINEL, "[]", None)`(见 Step 2 新 repo 方法);重新 `get_by_id` 得 row2
- `spawn_pty(id, SHELL_SENTINEL, &[], &row2.cwd, env=None, cols, rows, vec![], None)`;失败则 `update_status(exited)` 返回 Err
- `clear_scrollback``update_status("running", None)``emit_updated``arm_supervision`
- [ ] **Step 2**repo 加 `async fn update_command(&self, id, command: &str, args: &str, backend: Option<&str>) -> Result<(), DbError>`trait + sqlite `UPDATE ... SET command=?, args=?, backend=?, updated_at=?` + MemRepo)。
- [ ] **Step 3**:测试 `relaunch_as_shell_swaps_command_and_emits_updated`:建 `cat` 会话 → relaunch_as_shell → row.command == `$SHELL`、last_status==running、捕获到 `terminal.updated`
- [ ] **Step 4**`cargo test -p nomifun-terminal relaunch_as_shell`
- [ ] **Step 5**:提交。
### Task B2:路由 `POST /api/terminals/{id}/relaunch-shell`
**Files:** Modify `crates/backend/nomifun-terminal/src/routes.rs`
- [ ] **Step 1**:加 route + handler `relaunch_shell_terminal`(仿 `relaunch_terminal`,调 `relaunch_as_shell`)。
- [ ] **Step 2**`cargo build -p nomifun-terminal`
- [ ] **Step 3**:提交。
---
## Part A — 功能① 自动标题
### Task A1`title.rs`(提示词 + completer trait + Live 实现)
**Files:**
- Create: `crates/backend/nomifun-terminal/src/title.rs`
- Modify: `crates/backend/nomifun-terminal/src/lib.rs``mod title;` + re-export
- Modify: `crates/backend/nomifun-terminal/Cargo.toml`(加 `nomifun-ai-agent`
**Interfaces:**
- Produces:
- `pub trait TerminalTitleCompleter: Send + Sync { async fn summarize(&self, content: &str) -> Result<String, AppError>; }`
- `pub struct LiveTerminalTitleCompleter { provider_repo, encryption_key:[u8;32], workspace:PathBuf }`(仿 `LiveKnowledgeCompleter``resolve_default_model` + `one_shot_completion(TITLE_SYSTEM, content, 64)`)。
- `pub fn fallback_title(input: &str, n: usize) -> String`(取首行、去控制字符、截断 N 字,trim)。
- `const TITLE_SYSTEM`:要求只输出 ≤6 词/≤20 字的工作内容短标题,无标点结尾、无引号、无解释。
- [ ] **Step 1**:写 `fallback_title` 失败测试(首行截断、去 `\r\n`、去 ANSI/控制符、空输入返回空)。
- [ ] **Step 2**:实现 `fallback_title` + `TITLE_SYSTEM` + trait + `LiveTerminalTitleCompleter`(依 knowledge_completer.rs 拷贝 resolve/complete)。
- [ ] **Step 3**Cargo.toml 加依赖;lib.rs `mod title; pub use title::{...};`
- [ ] **Step 4**`cargo test -p nomifun-terminal title::`
- [ ] **Step 5**:提交。
### Task A2service 接入触发(首行捕获 + TurnEnd + 一次性守卫)
**Files:** Modify `crates/backend/nomifun-terminal/src/service.rs`
**Interfaces:**
- Consumes: `title::{TerminalTitleCompleter, fallback_title}``default_name``update_meta`
- Produces:
- 字段 `title_completer: Arc<RwLock<Option<Arc<dyn TerminalTitleCompleter>>>>``titled: Arc<DashMap<i64,()>>``first_input: Arc<DashMap<i64,String>>`
- `pub fn with_title_completer(&self, c: Arc<dyn TerminalTitleCompleter>)`
- `fn capture_first_input(&self, id, bytes)`(input() 内调用,累积首行到换行,封顶 ~200 字)。
- `async fn maybe_autotitle(&self, id, llm_source: Option<String>)``titled` 守卫 + `name==default_name` 守卫 → 有 completer 且有 llm_source 则 `summarize`,否则/失败 `fallback_title(first_input)`;非空才 `update_meta`
- [ ] **Step 1**`input()` 内(写 PTY 后)调 `capture_first_input`shell 路径:首行收齐后 `tokio::spawn``maybe_autotitle(id, None)`
- [ ] **Step 2**`spawn_pty` 的 lifecycle 消费者(515-529):收到首个 `TurnEnd` 时取 `payload["last_assistant_message"]``spawn``maybe_autotitle(id, Some(msg))`
- [ ] **Step 3**:守卫实现:先查 `titled.insert(id,()).is_none()` 抢占(已存在则 return);reload row,比对 `row.name == default_name(row.command, row.backend)`,不等则 return(用户改过名或已命名)。
- [ ] **Step 4**:测试:①无 completer 时 shell 首行 `echo hi` → 标题变 `hi`fallback);② 注入 fake completer → maybe_autotitle 用其结果;③ 已改名(name!=default)→ 不覆盖;④ 触发两次只生效一次。
- [ ] **Step 5**`cargo test -p nomifun-terminal autotitle`
- [ ] **Step 6**:提交。
### Task A3services.rs 注入 Live completer
**Files:** Modify `crates/backend/nomifun-app/src/services.rs`
- [ ] **Step 1**:在 terminal 接线块(440-459)后:`terminal_service.with_title_completer(Arc::new(LiveTerminalTitleCompleter { provider_repo: provider_repo.clone(), encryption_key, workspace: data_dir.clone() }));``nomifun_terminal::LiveTerminalTitleCompleter`)。
- [ ] **Step 2**`cargo build -p nomifun-app`
- [ ] **Step 3**:提交。
---
## Part D — 功能② 前端(reset / 回退按钮 / 连续 Ctrl+C / 重连重放)
### Task D1IPC + WS 桥
**Files:** Modify `ui/src/common/adapter/ipcBridge.ts``ui/src/common/adapter/httpBridge.ts`
- [ ] **Step 1**`ipcBridge.terminal``relaunchShell: httpPost<TerminalSession, void>('/api/terminals/{id}/relaunch-shell')`(仿 `relaunch`,按现有写法填 id)。
- [ ] **Step 2**`httpBridge.ts` WS `open` 处理:若为重连(attempt>0 / 曾断开),向监听器派发内部事件(如新增 `wsEmitter.emitReconnected()` 或复用一个 `terminal.__ws_reconnected` 名)。导出供 XtermView 订阅。
- [ ] **Step 3**`tsc` 通过;提交。
### Task D2XtermView reset + 重连重放 + 连续 Ctrl+C
**Files:** Modify `ui/src/renderer/pages/terminal/XtermView.tsx`+ 纯函数测试文件)
- [ ] **Step 1**`XtermViewHandle``reset: () => void`,实现 `term.reset()`
- [ ] **Step 2**:订阅 WS 重连事件:回调里 `term.reset()` 后重新 `ipcBridge.terminal.get(sessionId)`,用现有 streaming decoder 重放 `scrollback_b64`
- [ ] **Step 3**:连续 Ctrl+C 检测纯函数 `bumpCtrlC(state, nowMs, windowMs, threshold)` + 测试;onData 里识别 `\x03`,达阈值回调 `onEscalateShell`
- [ ] **Step 4**`cd ui && bun test`(纯函数);`tsc`;提交。
### Task D3TerminalSessionPage 回退 Shell 入口 + 接线
**Files:** Modify `ui/src/renderer/pages/terminal/TerminalSessionPage.tsx`、i18n `terminal.json`zh-CN/en-US
- [ ] **Step 1**:会话头加「回退 Shell」按钮(**不**受 `isExited` 限制),点击 `ipcBridge.terminal.relaunchShell(id)``xtermApi.current?.reset()`
- [ ] **Step 2**:连接 XtermView `onEscalateShell` → 同上回退逻辑 + 提示条 i18n `terminal.fallbackShellHint`
- [ ] **Step 3**i18n 加 `fallbackShell`/`fallbackShellHint`(中英)。
- [ ] **Step 4**`tsc`;提交。
---
## 自检(写完计划后)
- 覆盖:①标题(A1-A3)②回退(B1-B2 后端 + D1-D3 前端)③清理(C1-C4)—— 三需求均有任务。
- 类型一致:`delete_all`/`shutdown_cleanup`/`relaunch_as_shell`/`with_title_completer`/`relaunchShell` 跨任务命名一致。
- 兜底:标题无模型/失败 → `fallback_title(first_input)`,已在 A2 覆盖。