Add chapter reorder, search replace, inspiration capture, and AI chat with context editing, settings, offline handling, and naming checks. Fix dev black screen by keeping process.env reads in the main process only. Co-authored-by: Cursor <cursoragent@cursor.com>
24 KiB
笔临 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:
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
npm run test -- tests/main/chapter-move.test.ts
Expected: FAIL chapters.move is not a function
- Step 3: 实现
ChapterRepository.move
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:
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: 验证
npm run test -- tests/main/chapter-move.test.ts
npm run build
npm run test:e2e -- e2e/p21-features.spec.ts
- Step 8: Commit
git commit -am "feat: add chapter drag reorder within and across volumes"
Task 2: 搜索替换 UI(P2.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 下方增加:
<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:
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
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"。
语音按钮:
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:
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
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 扩展
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
npm run test -- tests/main/migrate-v3.test.ts
git commit -am "feat: add schema v3 and AI config types"
Task 5: AiClientService(P3.0)
Files:
-
Create:
src/main/services/ai-client.service.ts -
Create:
tests/main/ai-client.test.ts -
Step 1: 写真实 LM Studio 集成测试(禁止 skip)
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
npm run test -- tests/main/ai-client.test.ts
- Step 3: 实现 AiClientService
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 运行)
npm run test -- tests/main/ai-client.test.ts
- Step 5: Commit
git commit -am "feat: add AiClientService with LM Studio OpenAI compatibility"
Task 6: AiSessionRepository + IPC(P3.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 接流式):
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: 验证
npm run test -- tests/main/ai-session.repo.test.ts
- Step 6: Commit
git commit -am "feat: add AI session repository and IPC handlers"
Task 7: 流式 IPC + NetworkMonitor(P3.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 内:
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 订阅
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
git commit -am "feat: add AI streaming IPC and network monitor"
Task 8: AiPanel UI + useAiStore(P3.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
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
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:
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
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
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):
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
git commit -am "feat: add offline fallback, naming check, and AI snapshots"
Task 12: i18n + CSS + 全量测试 + v0.3.0(P3.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: 全量测试
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
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 6–7 |
| §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 1–12 |
| §10 DoD | Task 12 |
| 检查项 | 结果 |
|---|---|
| Placeholder 扫描 | 无 TBD;写作模式 Tab 在 Task 8 明确 comingSoon |
| 测试 Mock | 全文禁止 skip;ai-client.test 真实 HTTP |
| 类型一致 | AiContextBinding / context_json / IPC 命名一致 |
执行方式
Plan 已保存至 docs/superpowers/plans/2026-07-07-bilin-p21-p3.md。
两种执行选项:
- Subagent-Driven(推荐) — 每个 Task 派发独立 subagent,任务间 review,迭代快
- Inline Execution — 在本会话按 Task 顺序直接实现,批次间设检查点
你选哪种?