# Lynn v0.79.1 综合 Review (2026-05-25)

> 三路 audit(architecture / desktop UX / security)+ 我自己的 review 合成。
> 这次 session 实施了 6 项,并由 Codex 追加验收修复 2 项；剩余架构拆分继续交给 Codex 接班。

---

## 这次 session 已实施 ✅

| # | 优先级 | 项 | commits |
|---|---|---|---|
| 1 | P0-1(server 部分) | brain-v2-mirror cascade route metadata(fallback_from chain + lynn.provider SSE 富集) | `aaa2b79` |
| 2 | P0-2 | model-downloader.cjs DNS rebinding defense(`_dnsLookupBlockingPrivate` lookup) | `aaa2b79` |
| 3 | P1-1 | ProviderDetail.tsx memory-adaptive upgrade tier(`mem > 32` 藏 4B / `< 22` 藏 35B) | `aaa2b79` |
| 4 | P1-2 | server/routes/fs.js TOCTOU symlink-swap fix(canonical path 锁定写入) | `aaa2b79` |
| 5 | P1-3 | desktop/main.cjs browser:evaluate 加固(4000 字符 cap / 完整 audit log / sensitive deny env) | `aaa2b79` |
| 6 | P2-3 | brain-v2-mirror/auth.js nonce cache LRU + device check 顺序 reorder | `5a711df`, Codex follow-up |
| 7 | V0.79.2 | 本地 Qwen3.5 direct runner 拆分 + thinking-only 空正文自动 thinking-off retry/fallback | `430c549` |
| 8 | V0.79.2 | 天气工具日期窗口收敛：未来两天不含今天,输出“问题匹配日期”与降雨判断 | `dc556e7` |

测试覆盖:`brain-v2-mirror` 定向 16 files / 123 passed；Lynn 主仓全量 174 files / 1403 passed / 1 skipped；TypeScript `tsc --noEmit` 通过。

---

## 交给 codex 接班 🚧

按 ROI / 工作量排序。每项都是独立 PR-able 范围。

### **P0-3 · `server/routes/chat.js` 1965 行 god-object 拆解**(高 ROI · 4h)

8958149 的 refactor 已退化:从 765 → 1965 行,`createChatRoute` 里 35 个 inner function 全 closure 在 `sessionState` / `lifecycleHooks` / `engine` / `hub` / `broadcast` 上。

**拆解方案**:
```
server/chat/stream-emitters.js   ← emitStreamEvent / emitVisibleTextDelta /
                                   emitTrustedVisibleTextDelta /
                                   flushBufferedAssistantText 等 ~300 行
                                   (chat.js 520-816)
server/chat/turn-fallbacks.js    ← closeStreamWithVisibleFallback /
                                   finalizeReturnedTurnWithoutStream /
                                   scheduleReturnedTurnFinalizationFallback /
                                   scheduleTurnHardAbort /
                                   scheduleToolFinalizationFallback 等 ~250 行
                                   (chat.js 244-489)
server/chat/local-qwen35-bridge.js ← streamLocalQwen35DirectBridge 单独 180 行
                                   (chat.js 863)
```

Pass 一个 `{ ss, sessionPath, broadcast, lifecycleHooks }` context 对象,避免重复 closure。

**目标**:chat.js 拉回 ~900 行,变成 route + dispatch shell,业务逻辑在子模块。

---

### **P0-1 · Desktop 端 UI 消费 lynn.provider event**(高 UX 价值 · 3h)

server-side 这次已经发完整 `fallback_from: [{id, reason}]` 数组到 SSE `lynn.provider` event,但 **desktop 端 0 行代码消费**。

**实施清单**:
1. 找 desktop SSE consumer(可能在 `core/llm-client.js` parseSseJsonEvents 或专门 brain v2 consumer)
2. 解析 `object === 'lynn.provider'` event,把 `meta.active_provider` + `meta.fallback_from` 存到 turn / message state
3. `AssistantMessage.tsx` 头像下加一行 chip:
   - 无 fallback:`Lynn Brain · MiMo`(灰色,可隐藏)
   - 有 fallback:`MiMo (cooldown) → Spark fallback · 1.2s`(暖橙,主动暴露)
4. 当 cascade 全死(`run` throws `all providers failed`):
   - 替换 empty hint("模型只想了想,没说出话来")
   - 改为双按钮卡:`Brain 全链路不可用 · [切换到本地 9B] [重试]`

**i18n**:5 个 locale 都要加 `cascadeFallback` / `cascadeAllDead` 字串。

---

### **P1-4 · `server/chat/report-research-context.js` 1175 行拆 3 文件**(中等 ROI · 1.5h)

1175 行做了 5 件事不该混:(a) intent 正则分类 (b) query 构造 (c) 4 个外部工具调用 (d) 6 种结果 parse (e) prose 合成。

**拆解方案**:
```
report-research-intent.js  ← regex + inferReportResearchKind +
                              extractStockTargetForResearch (~150 行 纯函数,可单测)
report-research-fetch.js   ← searchSummary + withTimeout + 4 个工具调用 (~250 行 async I/O)
report-research-answer.js  ← 6 个 parse* + 6 个 buildDirect*Answer (~700 行 纯文本)
```

**收益**:intent 测试不再需要 mock 网络,answer parser 可以独立用 fixture 跑回归。

---

### **P1-5 · Lazy-load KaTeX/mermaid/codemirror + markdown worker**(perf · 1d)

`utils/markdown.ts:8-10` eager import KaTeX + CSS。codemirror-vendor 1.5MB / mermaid 563K / rendering-vendor 637K 全 eager。冷启在低配机加载 ~2.7MB 没用的 vendor JS。

**实施清单**:
1. KaTeX:`hasMathBlock(text)` regex detect → 动态 `import('@traptitech/markdown-it-katex')` 后注册到 md 实例。需要把 `renderMarkdown` 改 async — 涉及组件树多处 caller 改 await。
2. Codemirror:只在首次 fenced ``` 代码块出现时 dynamic import。
3. Mermaid:类似,detect ` ```mermaid` 触发 lazy load。
4. `markdown.worker.js`:大文本 `renderMarkdown` 调用切 worker,主线程不 jank。

**注意**:这是 React 组件树 contract 改动,改 `async render` 会让 `Suspense` boundary 多处出现。建议跟 codex 设计一次,不要一来就 patch。

---

### **P2-1 · `core/` → `server/i18n.js` 反向依赖**(架构 · 1h)

9 个 `core/*.js` 文件直接 import `server/i18n.js`(`core/agent-manager.js` / `core/engine.js` / `core/llm-utils.js` / `core/execution-router.js` 等)。`core/` 应该 runtime-agnostic 可嵌 CLI / headless,但现在拖着 HTTP 层翻译。

**修法**:`core/engine.js` 构造时 inject `{ t, getLocale }` deps,9 个 import 改成 `this.t(...)`。测试/CLI 注 stub。

---

### **P2-2 · LocalQwen35Panel 900 行 + 8-branch button ternary**(UX · 2h)

`ProviderDetail.tsx:LocalQwen35Panel` 一个组件挤 11+ derived states,主 action 按钮用 8-branch nested ternary 切 label。用户体验:点"下载 9B 并启动"可能看到"下载中 27%"/"模型加载中"/"已启用,重新检查",无连贯叙事。

**修法**:把组件 state 抽成 finite state machine(`xstate` or 手写),按钮 label 由当前 state 决定,显式 transitions。

---

### **P2-4 · `process.env` 私模块化**(security defense-in-depth · 1-2h)

`lib/mcp-client.js` 的 SAFE_ENV_KEYS 限制 child spawn,但同 process 内任何代码 `console.log(process.env)` 仍会 dump 一切。建议把 provider keys 移到私模块 closure,读完从 `process.env` delete。

---

### **P2-5 · TPS p50/p95 dashboard**(observability · 1d)

为后续 cascade 决策提供数据(比如:连续 7 天 MiMo p95 > 3s 才考虑切 cascade 顺序)。

**实施**:
- `brain-v2-mirror/perf-init.js` 加 histogram(已有 undici metrics 接入点)
- `/v2/state` endpoint 暴露 metrics 给 Lynn 桌面端 dashboard
- 或者直接接 prometheus / 飞书巡检脚本

---

## 全部 commits 系列(本 session)

```
dc556e7 chore(v0.79.2): review handoff and weather window cleanup
5a711df fix(brain-v2): P2-3 nonce cache LRU + reorder device check before nonce remember
aaa2b79 fix(v0.79.1): P0+P1 security + UX hardening batch (DNS rebinding / TOCTOU / browser-eval / memory-adaptive / cascade route)
430c549 fix(v0.79.2): harden weather answers and local qwen fallback
2155f52 fix(brain-v2): stabilize Spark fallback health probe
9f040d6 docs(ops): scrub Tencent IP + Spark username to placeholders
c4fd4db fix(brain-v2): Spark APEX-MTP fallback — model id + default thinking-off
11b12eb docs(ops): Spark APEX-MTP canonical service + TPS regression baseline
8958149 chore(v0.79.1): harden local model release path [skip ci]
```

所有 commit 都在 `codex/v0791-architecture-cleanup` 分支本地,尚未 push origin。

---

## 测试矩阵

- `brain-v2-mirror` + safety/fallback 定向:16 files / **123 passed**
- `Lynn 主仓 npm test`:174 files / **1403 passed** / 1 skipped
- TypeScript `tsc --noEmit`: ✓
- 实战 e2e:Tencent 端 Brain v2 强制 mark MiMo cooldown → cascade 真去 Spark,冷启 TTFT 813ms / 热启 787ms / content 干净 / reasoning len=0

### Codex 验收追加

- `brain-v2-mirror/auth.js`:修正 nonce LRU 顺序,先检查 replay 再 evict,避免满表边界重放最老 nonce 被误放行；新增回归测试。
- `lib/tools/realtime-info.js`:修正“未来两天/明后天/明天后天”日期窗口,问题匹配区块排除今天；杭州未来两天现在输出 2026-05-26 与 2026-05-27。
- `server/routes/chat.js` direct local Qwen path:拆出 `server/chat/local-qwen35-direct-runner.js`,thinking-on 只产 reasoning 无正文时自动 thinking-off retry,仍无正文则给可见 fallback。

---

## 给 codex 的工作 prompt(可直接 copy)

> 接 Lynn v0.79.1 review 后续。文档 `docs/ops/v0.79.1-review-20260525.md` 列了 7 项交给你的工作。优先级:
> - **P0-3 chat.js 拆解**(高 ROI,4h,纯重构无业务变化)
> - **P0-1 desktop 端 cascade UI**(高用户价值,3h,跨 brain-v2 + desktop 联调)
> - **P1-4 report-research split**(中等 ROI,1.5h,纯重构)
> - **P1-5 lazy load**(perf,1d,涉及 React 组件树 async 改造)
> - **P2 系列**(polish,按精力排)
> 
> 每个 P 都独立 PR-able,不互相阻塞。chat.js 那个先做,因为它的 god-object 状态会让其他改动越来越难。
> 
> 我这次 session 改的 brain-v2-mirror server-side 已经发完整 `fallback_from` 到 SSE,P0-1 desktop 端只需 consume + render,不需要改 server。

---

*last updated: 2026-05-25 · 见 git log 跟踪后续变更*
