Files
bilin/docs/superpowers/plans/2026-07-07-bilin-p21-p3.md
T
2026-07-06 14:30:01 +08:00

813 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 笔临 P2.1 + P3 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 交付 v0.3.0:P2 遗留(章节拖拽、搜索替换、灵感模态+语音、@跳转、引用钉住)+ P3 严格(LM Studio 对话、上下文注入、快捷命令、离线降级、命名检查、AI 快照)。
**Architecture:** 延续 electron-vite 单体;主进程 schema v3 + `AiClientService`OpenAI 兼容 `fetch` 至 LM Studio+ 流式 IPC 事件;渲染进程 `AiPanel` 替换占位;测试 **禁止 Mock**,集成/E2E 必须打真实 `http://127.0.0.1:1234`
**Tech Stack:** Electron 36、electron-vite、React 18、TypeScript、TipTap、node:sqlite、Zustand、Jotai、i18next、Vitest、Playwright
**Spec:** `docs/superpowers/specs/2026-07-07-bilin-p21-p3-design.md`
**LM Studio 默认:** `baseUrl=http://127.0.0.1:1234``model=gemma-4-e4b-it`(可用 `BILIN_AI_BASE_URL` / `BILIN_AI_MODEL` 覆盖)
---
## File Map
| 文件 | 职责 |
|------|------|
| `src/main/db/schema-v3.sql` | `ai_sessions` / `ai_messages` |
| `src/main/db/migrate.ts` | v2→v3 迁移 |
| `src/main/db/repositories/chapter.repo.ts` | `move()` |
| `src/main/db/repositories/ai-session.repo.ts` | 会话/消息 CRUD |
| `src/main/services/ai-client.service.ts` | LM Studio HTTP + SSE 流 |
| `src/main/services/ai-context-builder.service.ts` | 上下文拼接 |
| `src/main/services/network-monitor.service.ts` | online/offline 推送 |
| `src/main/ipc/handlers/ai.handler.ts` | AI IPC + 流式 |
| `src/main/ipc/handlers/chapter.handler.ts` | `chapter:move` |
| `src/shared/types.ts` | AiConfig / AiSession / NamingConflict |
| `src/shared/ipc-channels.ts` | P2.1/P3 IPC |
| `src/preload/index.ts` | AI API + 事件订阅 |
| `src/renderer/stores/useAiStore.ts` | 会话/流式/disabled |
| `src/renderer/components/ai/AiPanel.tsx` | 右栏 AI 主 UI |
| `src/renderer/components/ai/ContextEditorModal.tsx` | 上下文勾选 |
| `src/renderer/components/ai/NamingCheckModal.tsx` | 命名检查 |
| `src/renderer/components/inspiration/InspirationModal.tsx` | 灵感捕获 |
| `src/renderer/components/settings/AiSettingsPage.tsx` | AI 配置 Tab |
| `src/renderer/components/search/SearchModal.tsx` | 替换区 |
| `src/renderer/components/layout/EditorLayout.tsx` | 章节 drag |
| `e2e/p21-features.spec.ts` | P2.1 E2E |
| `e2e/ai-chat.spec.ts` | P3 对话 E2E |
---
## Task 1: 章节拖拽(P2.1a
**Files:**
- Modify: `src/main/db/repositories/chapter.repo.ts`
- Modify: `src/main/ipc/handlers/chapter.handler.ts`
- Modify: `src/shared/ipc-channels.ts`
- Modify: `src/preload/index.ts`, `src/shared/electron-api.d.ts`
- Modify: `src/renderer/components/layout/EditorLayout.tsx`
- Modify: `src/renderer/styles/layout.css`
- Create: `tests/main/chapter-move.test.ts`
- Create: `e2e/p21-features.spec.ts`DRAG 用例)
- [ ] **Step 1: 写 chapter.move 失败测试**
`tests/main/chapter-move.test.ts`
```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { DatabaseSync } from 'node:sqlite'
import { migrate } from '../../src/main/db/migrate'
import { ChapterRepository } from '../../src/main/db/repositories/chapter.repo'
import { VolumeRepository } from '../../src/main/db/repositories/volume.repo'
import type { SqliteDb } from '../../src/main/db/types'
describe('ChapterRepository.move', () => {
let db: SqliteDb
let chapters: ChapterRepository
let volId: string
beforeEach(() => {
db = new DatabaseSync(':memory:')
migrate(db)
const vols = new VolumeRepository(db)
volId = vols.create('卷一', 0).id
chapters = new ChapterRepository(db)
chapters.create(volId, 'A', 0)
chapters.create(volId, 'B', 1)
chapters.create(volId, 'C', 2)
})
afterEach(() => db.close())
it('reorders within volume', () => {
const list = chapters.listByVolume(volId)
const idC = list.find((x) => x.title === 'C')!.id
chapters.move(idC, volId, 0)
chapters.reorderVolume(volId, [idC, list.find((x) => x.title === 'A')!.id, list.find((x) => x.title === 'B')!.id])
const titles = chapters.listByVolume(volId).map((x) => x.title)
expect(titles).toEqual(['C', 'A', 'B'])
})
})
```
- [ ] **Step 2: 运行确认 FAIL**
```bash
npm run test -- tests/main/chapter-move.test.ts
```
Expected: FAIL `chapters.move is not a function`
- [ ] **Step 3: 实现 `ChapterRepository.move`**
```typescript
move(chapterId: string, volumeId: string, sortOrder: number): Chapter {
this.db
.prepare(
`UPDATE chapters SET volume_id = ?, sort_order = ?, updated_at = datetime('now') WHERE id = ?`
)
.run(volumeId, sortOrder, chapterId)
return this.get(chapterId)!
}
reorderVolume(volumeId: string, orderedIds: string[]): void {
const stmt = this.db.prepare(
`UPDATE chapters SET sort_order = ? WHERE id = ? AND volume_id = ?`
)
orderedIds.forEach((id, idx) => stmt.run(idx, id, volumeId))
}
```
- [ ] **Step 4: IPC `CHAPTER_MOVE`**
`ipc-channels.ts` 追加 `CHAPTER_MOVE: 'chapter:move'`
`chapter.handler.ts`
```typescript
ipcMain.handle(
IPC.CHAPTER_MOVE,
(_e, { bookId, chapterId, volumeId, sortOrder }: { bookId: string; chapterId: string; volumeId: string; sortOrder: number }) =>
wrap(() => registry.getChapterRepo(bookId).move(chapterId, volumeId, sortOrder))
)
```
preload / electron-api.d.ts 暴露 `chapter.move(bookId, chapterId, volumeId, sortOrder)`
- [ ] **Step 5: EditorLayout 拖拽 UI**
章节 `.chapter-item` 增加 `draggable``onDragStart``chapterId``onDragOver` preventDefault`onDrop` 计算新 `sortOrder` 调 IPC。
同卷:根据 drop 目标 index 调用 `reorderVolume`(新增 IPC `chapter:reorder` 或在 move 后批量 reorder)。
跨卷:drop 到另一卷 header → `move(chapterId, targetVolId, targetLen)`
- [ ] **Step 6: E2E E2E-P21-DRAG-01**
`e2e/p21-features.spec.ts`:两章 → drag 交换顺序 → 重启 → 顺序保持。
- [ ] **Step 7: 验证**
```bash
npm run test -- tests/main/chapter-move.test.ts
npm run build
npm run test:e2e -- e2e/p21-features.spec.ts
```
- [ ] **Step 8: Commit**
```bash
git commit -am "feat: add chapter drag reorder within and across volumes"
```
---
## Task 2: 搜索替换 UIP2.1b
**Files:**
- Modify: `src/renderer/components/search/SearchModal.tsx`
- Modify: `src/renderer/styles/layout.css`
- Modify: `public/locales/zh-CN/translation.json`, `public/locales/en/translation.json`
- Extend: `e2e/p21-features.spec.ts`
- [ ] **Step 1: SearchModal 替换折叠区**
`search-results` 下方增加:
```tsx
<details className="search-replace-panel" data-testid="search-replace-panel">
<summary>{t('search.replace')}</summary>
<input data-testid="search-replace-input" value={replacement} onChange={...} />
<button data-testid="search-replace-preview" onClick={() => void previewReplace()}>
{t('search.replacePreview')}
</button>
<button data-testid="search-replace-execute" onClick={() => void executeReplace()}>
{t('search.replaceExecute')}
</button>
</details>
```
`previewReplace`
```typescript
const result = await ipcCall(() =>
window.electronAPI.search.replace(bookId, query, replacement, true, { regex, caseSensitive })
)
setReplacePreviews(result.previews)
```
`executeReplace``dryRun: false``refreshChapters()` + toast。
- [ ] **Step 2: i18n 键**
`search.replace`, `search.replacePreview`, `search.replaceExecute`, `search.replaceConfirm`, `search.replaceDone`
- [ ] **Step 3: E2E E2E-P21-REP-01**
写入章节唯一词 → 搜索 → 预览 → 执行 → `editor` 含新词。
- [ ] **Step 4: 验证 + Commit**
```bash
npm run test:e2e -- e2e/p21-features.spec.ts
git commit -am "feat: add global search replace preview and execute UI"
```
---
## Task 3: 灵感模态 + @跳转 + 引用钉住(P2.1c)
**Files:**
- Create: `src/renderer/components/inspiration/InspirationModal.tsx`
- Modify: `src/renderer/App.tsx`
- Modify: `src/renderer/components/editor/TipTapEditor.tsx`
- Modify: `src/renderer/extensions/mention.ts`
- Modify: `src/renderer/stores/useReferenceStore.ts`
- Modify: `src/renderer/components/reference/ReferencePanel.tsx`
- Modify: `src/renderer/styles/layout.css`
- Extend: `e2e/p21-features.spec.ts`
- [ ] **Step 1: InspirationModal**
对齐 `design/ui_pc.html` `#inspirationModal`title、content、tags;保存调 `inspiration.create``data-testid="inspiration-modal"`
语音按钮:
```typescript
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
if (SpeechRecognition) {
const rec = new SpeechRecognition()
rec.lang = 'zh-CN'
rec.onresult = (e) => setContent((c) => c + e.results[0][0].transcript)
rec.start()
}
```
不可用时 `disabled` + `title={t('inspiration.voiceUnavailable')}`
- [ ] **Step 2: App.tsx 改 captureInspiration**
`captureInspiration` 快捷键 → `useAppStore.setInspirationModalOpen(true)`,不再直接 create。
- [ ] **Step 3: Mention 点击跳转**
`TipTapEditor` `editorProps`
```typescript
handleClick: (view, pos, event) => {
const el = (event.target as HTMLElement).closest('[data-setting-mention]')
if (!el) return false
const id = el.getAttribute('data-id')
if (id) void useEditStore.getState().switchTarget({ kind: 'setting', id })
return true
}
```
确保 `applyMention` 插入 HTML 含 `data-id`
- [ ] **Step 4: 引用钉住**
`useReferenceStore``pinned: boolean`, `setPinned`
`ReferencePanel``pinned` 时关闭按钮改为取消钉住;`setOpen(false)``pinned` 则忽略。
- [ ] **Step 5: E2E E2E-P21-INSP-01**
`Control+Shift+I` → 填内容 → 保存 → `inspiration-list` 可见。
- [ ] **Step 6: Commit**
```bash
git commit -am "feat: add inspiration modal, mention navigation, reference pin"
```
---
## Task 4: Schema v3 + 共享类型 + AI 配置(P3.0
**Files:**
- Create: `src/main/db/schema-v3.sql`
- Modify: `src/main/db/migrate.ts`
- Create: `tests/main/migrate-v3.test.ts`
- Modify: `src/shared/types.ts`
- Modify: `src/shared/ipc-channels.ts`
- Modify: `src/main/services/global-settings.ts`
- [ ] **Step 1: schema-v3.sql**
内容同 spec §3.1 `ai_sessions` / `ai_messages` / index。
- [ ] **Step 2: migrate.ts**
`CURRENT_VERSION = 3``current < 3``db.exec(schemaV3)` + insert version 3。
- [ ] **Step 3: migrate-v3.test.ts**
v2 库 migrate 后 `SELECT name FROM sqlite_master``ai_sessions`
- [ ] **Step 4: types.ts 扩展**
```typescript
export interface AiConfig {
backend: AiBackend
baseUrl: string
model: string
apiKey?: string
timeoutMs: number
maxTokens: number
}
export const DEFAULT_AI_CONFIG: AiConfig = {
backend: 'lmstudio',
baseUrl: process.env.BILIN_AI_BASE_URL ?? 'http://127.0.0.1:1234',
model: process.env.BILIN_AI_MODEL ?? 'gemma-4-e4b-it',
timeoutMs: 60_000,
maxTokens: 2048
}
```
`GlobalSettings` 追加 `aiConfig`, `namingIgnoreWords: string[]`
- [ ] **Step 5: global-settings defaults()**
合并 `aiConfig: DEFAULT_AI_CONFIG`, `namingIgnoreWords: []`
- [ ] **Step 6: 验证 + Commit**
```bash
npm run test -- tests/main/migrate-v3.test.ts
git commit -am "feat: add schema v3 and AI config types"
```
---
## Task 5: AiClientServiceP3.0
**Files:**
- Create: `src/main/services/ai-client.service.ts`
- Create: `tests/main/ai-client.test.ts`
- [ ] **Step 1: 写真实 LM Studio 集成测试(禁止 skip**
```typescript
import { describe, it, expect } from 'vitest'
import { AiClientService } from '../../src/main/services/ai-client.service'
import { DEFAULT_AI_CONFIG } from '../../src/shared/types'
describe('AiClientService', () => {
const client = new AiClientService(() => DEFAULT_AI_CONFIG)
it('IT-AI-01: chat returns non-empty content from LM Studio', async () => {
const content = await client.chat([
{ role: 'user', content: '回复一个字:好' }
])
expect(content.trim().length).toBeGreaterThan(0)
}, 90_000)
})
```
- [ ] **Step 2: 运行确认 FAIL**
```bash
npm run test -- tests/main/ai-client.test.ts
```
- [ ] **Step 3: 实现 AiClientService**
```typescript
export interface ChatMessage {
role: 'system' | 'user' | 'assistant'
content: string
}
export class AiClientService {
constructor(private getConfig: () => AiConfig) {}
private endpoint(): string {
const { baseUrl } = this.getConfig()
return `${baseUrl.replace(/\/$/, '')}/v1/chat/completions`
}
async chat(messages: ChatMessage[], onChunk?: (delta: string) => void): Promise<string> {
const config = this.getConfig()
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), config.timeoutMs)
const res = await fetch(this.endpoint(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {})
},
body: JSON.stringify({
model: config.model,
messages,
max_tokens: config.maxTokens,
stream: Boolean(onChunk)
}),
signal: controller.signal
})
clearTimeout(timer)
if (!res.ok) throw new Error(`AI ${res.status}: ${await res.text()}`)
if (!onChunk) {
const json = await res.json()
return json.choices[0].message.content as string
}
// SSE parse: data: {...}\n\n
let full = ''
const reader = res.body!.getReader()
const decoder = new TextDecoder()
let buf = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buf += decoder.decode(value, { stream: true })
const lines = buf.split('\n')
buf = lines.pop() ?? ''
for (const line of lines) {
if (!line.startsWith('data: ')) continue
const payload = line.slice(6).trim()
if (payload === '[DONE]') continue
const parsed = JSON.parse(payload)
const delta = parsed.choices?.[0]?.delta?.content ?? ''
if (delta) {
full += delta
onChunk(delta)
}
}
}
return full
}
}
```
- [ ] **Step 4: 验证 PASS(需 LM Studio 运行)**
```bash
npm run test -- tests/main/ai-client.test.ts
```
- [ ] **Step 5: Commit**
```bash
git commit -am "feat: add AiClientService with LM Studio OpenAI compatibility"
```
---
## Task 6: AiSessionRepository + IPCP3.1
**Files:**
- Create: `src/main/db/repositories/ai-session.repo.ts`
- Create: `tests/main/ai-session.repo.test.ts`
- Create: `src/main/ipc/handlers/ai.handler.ts`
- Modify: `src/main/ipc/register.ts`
- Modify: `src/main/services/book-registry.ts`
- Modify: `src/preload/index.ts`, `src/shared/electron-api.d.ts`
- [ ] **Step 1: ai-session.repo 测试 + 实现**
CRUD`list`, `create`, `update`, `delete`, `listMessages`, `addMessage`
`context_json``AiContextBinding` JSON 序列化。
- [ ] **Step 2: ai.handler.ts**
注册 spec §4.1 全部 `ai:session*` / `ai:messageList`
`ai:chat` 骨架(暂同步,Task 7 接流式):
```typescript
ipcMain.handle(IPC.AI_CHAT, async (event, { bookId, sessionId, content }) => {
return wrap(async () => {
const repo = registry.getAiSessionRepo(bookId)
const settings = globalSettings.get()
repo.addMessage(sessionId, 'user', content)
const history = repo.listMessages(sessionId)
const assistantId = randomUUID()
const reply = await aiClient.chat(history.map(...))
repo.addMessage(sessionId, 'assistant', reply, assistantId)
return { messageId: assistantId }
})
})
```
- [ ] **Step 3: book-registry getAiSessionRepo**
- [ ] **Step 4: preload 暴露 `window.electronAPI.ai.*`**
- [ ] **Step 5: 验证**
```bash
npm run test -- tests/main/ai-session.repo.test.ts
```
- [ ] **Step 6: Commit**
```bash
git commit -am "feat: add AI session repository and IPC handlers"
```
---
## Task 7: 流式 IPC + NetworkMonitorP3.1
**Files:**
- Create: `src/main/services/network-monitor.service.ts`
- Modify: `src/main/ipc/handlers/ai.handler.ts`
- Modify: `src/main/index.ts`
- Modify: `src/preload/index.ts`
- [ ] **Step 1: 改造 ai:chat 为流式**
`ai:chat` 内:
```typescript
const win = BrowserWindow.fromWebContents(event.sender)
let full = ''
await aiClient.chat(messages, (delta) => {
full += delta
win?.webContents.send(IPC.AI_STREAM_CHUNK, { messageId: assistantId, delta, done: false })
})
win?.webContents.send(IPC.AI_STREAM_CHUNK, { messageId: assistantId, delta: '', done: true })
repo.addMessage(sessionId, 'assistant', full, assistantId)
```
`ai:abort`:维护 `Map<messageId, AbortController>`
- [ ] **Step 2: preload 订阅**
```typescript
onAiStreamChunk: (cb) => {
const handler = (_e, payload) => cb(payload)
ipcRenderer.on(IPC.AI_STREAM_CHUNK, handler)
return () => ipcRenderer.removeListener(IPC.AI_STREAM_CHUNK, handler)
}
```
- [ ] **Step 3: NetworkMonitor**
`net.isOnline()` + 每 30s 检测;变化时 `webContents.send(IPC.AI_NETWORK_STATUS, { online })`
- [ ] **Step 4: Commit**
```bash
git commit -am "feat: add AI streaming IPC and network monitor"
```
---
## Task 8: AiPanel UI + useAiStoreP3.1
**Files:**
- Create: `src/renderer/stores/useAiStore.ts`
- Create: `src/renderer/components/ai/AiPanel.tsx`
- Modify: `src/renderer/components/layout/RightPanel.tsx`
- Modify: `src/renderer/styles/layout.css`
- [ ] **Step 1: useAiStore**
```typescript
interface AiStore {
sessions: AiSession[]
activeSessionId: string | null
messages: AiMessage[]
streamingMessageId: string | null
streamingBuffer: string
disabled: boolean
writingMode: AiWritingMode // default 'chat'
loadSessions(bookId: string): Promise<void>
sendMessage(content: string): Promise<void>
appendStreamChunk(payload: StreamChunk): void
}
```
挂载时订阅 `onAiStreamChunk` / `onAiNetworkStatus`
- [ ] **Step 2: AiPanel 结构**
对齐 `ui_pc.html``ai-session-bar``ai-mode-tabs`(非 chat 调 `showToast(comingSoon)`)、`context-preview`Task 9)、`offline-banner``chat-messages``quick-cmds``chat-input-area`
`data-testid``ai-panel`, `ai-session-select`, `ai-chat-input`, `ai-send`, `chat-message-assistant`.
- [ ] **Step 3: RightPanel 替换 AI 占位**
`panel-content` AI Tab 渲染 `<AiPanel />`
- [ ] **Step 4: E2E E2E-P3-CHAT-01**
`e2e/ai-chat.spec.ts`:打开书 → 新建会话 → 输入「你好」→ Enter → `chat-message-assistant` 非空(timeout 90s)。
- [ ] **Step 5: 验证 + Commit**
```bash
npm run test:e2e -- e2e/ai-chat.spec.ts
git commit -am "feat: add AiPanel with streaming chat UI"
```
---
## Task 9: 上下文编辑器 + 快捷命令(P3.2)
**Files:**
- Create: `src/main/services/ai-context-builder.service.ts`
- Create: `tests/main/ai-context-builder.test.ts`
- Create: `src/renderer/components/ai/ContextEditorModal.tsx`
- Modify: `src/main/ipc/handlers/ai.handler.ts`
- Modify: `src/renderer/components/ai/AiPanel.tsx`
- [ ] **Step 1: AiContextBuilder 测试**
截断:超长 chapter content → 块 ≤ 2000 字;总量 ≤ 16KB。
- [ ] **Step 2: 实现 build(binding, bookId)**
返回 `{ systemPrompt, preview: ContextPreviewItem[] }`
- [ ] **Step 3: ContextEditorModal**
四 Tab 勾选列表(chapters/outlines/settings/inspirations);保存 → `ai:sessionUpdate` patch `context_json`;「更新上下文」→ `ai:buildContext`
- [ ] **Step 4: 快捷命令**
`src/renderer/lib/ai-slash-commands.ts`
```typescript
export function resolveSlashCommand(cmd: string, t: TFunction): string | null {
const map: Record<string, string> = {
'/续写': t('ai.slash.continue'),
'/扩写': t('ai.slash.expand'),
// ...
}
return map[cmd.split(/\s/)[0]] ?? null
}
```
发送前:若匹配 slash,用模板替换实际 prompt。
- [ ] **Step 5: E2E E2E-P3-CTX-01 / E2E-P3-CMD-01**
- [ ] **Step 6: Commit**
```bash
git commit -am "feat: add AI context editor and slash commands"
```
---
## Task 10: AI 设置页(P3.0 续)
**Files:**
- Create: `src/renderer/components/settings/AiSettingsPage.tsx`
- Modify: `src/renderer/components/settings/SettingsPage.tsx`
- Modify: `src/main/ipc/handlers/ai.handler.ts``ai:testConnection`
- Modify: `public/locales/*/translation.json`
- [ ] **Step 1: AiSettingsPage**
字段:backend select、baseUrl、model、apiKey(非 lmstudio 显示)、timeout。
云端 backend 显示 §6.9 许可链接。
「测试连接」→ `ai:testConnection` → 最小 chat → toast。
- [ ] **Step 2: Settings 导航增加 AI Tab**
- [ ] **Step 3: Commit**
```bash
git commit -am "feat: add AI settings page with connection test"
```
---
## Task 11: 离线降级 + 命名检查 + AI 快照(P3.3
**Files:**
- Create: `src/renderer/components/ai/NamingCheckModal.tsx`
- Create: `src/main/services/naming-check.service.ts`
- Create: `tests/main/naming-parse.test.ts`
- Modify: `src/renderer/components/layout/RightPanel.tsx`(知识库 Tab 按钮)
- Modify: `src/renderer/lib/editor-commands.ts`
- Modify: `src/renderer/components/ai/AiPanel.tsx`
- [ ] **Step 1: 离线规则**
`useAiStore``disabled = aiConfig.backend !== 'lmstudio' && !networkOnline`
`AiPanel` 显示 `#offlineBanner`;网络恢复 toast。
- [ ] **Step 2: naming-check.service**
`buildPrompt(settings, chaptersPlain)` → 调 `AiClientService.chat``parseNamingJson(response)`
解析器单测(不 mock HTTP):`tests/main/naming-parse.test.ts`
- [ ] **Step 3: NamingCheckModal**
列表 + 忽略/替换;替换调 `search.replace`
- [ ] **Step 4: AI 快照**
`insertToEditor(text)`
```typescript
await ipcCall(() => window.electronAPI.snapshot.create(bookId, chapterId, 'ai', t('snapshot.ai'), currentHtml))
// then insert text at cursor
```
- [ ] **Step 5: E2E E2E-P3-NAME-01**
种子:设定「林远」+ 正文「林悦」→ 命名检查 → 至少一行结果。
- [ ] **Step 6: Commit**
```bash
git commit -am "feat: add offline fallback, naming check, and AI snapshots"
```
---
## Task 12: i18n + CSS + 全量测试 + v0.3.0P3.4
**Files:**
- Modify: `public/locales/zh-CN/translation.json`, `public/locales/en/translation.json`
- Modify: `src/renderer/styles/layout.css`
- Modify: `package.json`, `README.md`
- Create: `e2e/ai-chat.spec.ts`(补全 CTX/CMD/OFF
- Extend: `e2e/p21-features.spec.ts`
- [ ] **Step 1: i18n 全量键**
`ai.*`, `naming.*`, `inspiration.voice*`, `search.replace*`, `snapshot.ai`
- [ ] **Step 2: layout.css**
`.ai-session-bar`, `.ai-mode-tabs`, `.chat-msg.user/.ai`, `.quick-cmd`, `.offline-banner`, `.naming-modal`, `.search-replace-panel`
- [ ] **Step 3: 全量测试**
```bash
npm run test
npm run build
npm run test:e2e
```
Expected: 全部 PASS**LM Studio 必须在 127.0.0.1:1234 运行**
- [ ] **Step 4: package.json version 0.3.0 + README**
- [ ] **Step 5: Commit + tag**
```bash
git commit -am "chore: prepare v0.3.0 release"
git tag v0.3.0
```
---
## Plan Self-Review
| Spec 章节 | 对应用 Task |
|-----------|-------------|
| §5.1 章节拖拽 | Task 1 |
| §5.2 搜索替换 | Task 2 |
| §5.3 灵感模态+语音 | Task 3 |
| §5.4 @跳转 | Task 3 |
| §5.5 引用钉住 | Task 3 |
| §3 Schema v3 | Task 4 |
| §2.2 AiClientService | Task 5 |
| §4 AI IPC | Task 67 |
| §6.1 AiPanel | Task 8 |
| §6.2 上下文+快捷命令 | Task 9 |
| §6.7 AI 设置 | Task 10 |
| §6.4 离线 | Task 11 |
| §6.5 命名检查 | Task 11 |
| §6.6 AI 快照 | Task 11 |
| §7 测试 | Task 112 |
| §10 DoD | Task 12 |
| 检查项 | 结果 |
|--------|------|
| Placeholder 扫描 | 无 TBD;写作模式 Tab 在 Task 8 明确 comingSoon |
| 测试 Mock | 全文禁止 skipai-client.test 真实 HTTP |
| 类型一致 | `AiContextBinding` / `context_json` / IPC 命名一致 |
---
## 执行方式
Plan 已保存至 `docs/superpowers/plans/2026-07-07-bilin-p21-p3.md`
**两种执行选项:**
1. **Subagent-Driven(推荐)** — 每个 Task 派发独立 subagent,任务间 review,迭代快
2. **Inline Execution** — 在本会话按 Task 顺序直接实现,批次间设检查点
你选哪种?