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

24 KiB
Raw Blame History

笔临 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 + AiClientServiceOpenAI 兼容 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:1234model=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.tsDRAG 用例)

  • 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 增加 draggableonDragStartchapterIdonDragOver preventDefaultonDrop 计算新 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: 搜索替换 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 下方增加:

<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)

executeReplacedryRun: falserefreshChapters() + 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 #inspirationModaltitle、content、tags;保存调 inspiration.createdata-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: 引用钉住

useReferenceStorepinned: boolean, setPinned

ReferencePanelpinned 时关闭按钮改为取消钉住;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 = 3current < 3db.exec(schemaV3) + insert version 3。

  • Step 3: migrate-v3.test.ts

v2 库 migrate 后 SELECT name FROM sqlite_masterai_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: AiClientServiceP3.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 + 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 测试 + 实现

CRUDlist, create, update, delete, listMessages, addMessage

context_jsonAiContextBinding 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 + 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 内:

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 + 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

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.htmlai-session-barai-mode-tabs(非 chat 调 showToast(comingSoon))、context-previewTask 9)、offline-bannerchat-messagesquick-cmdschat-input-area

data-testidai-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.tsai: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: 离线规则

useAiStoredisabled = aiConfig.backend !== 'lmstudio' && !networkOnline

AiPanel 显示 #offlineBanner;网络恢复 toast。

  • Step 2: naming-check.service

buildPrompt(settings, chaptersPlain) → 调 AiClientService.chatparseNamingJson(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.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: 全量测试
npm run test
npm run build
npm run test:e2e

Expected: 全部 PASSLM 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 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 顺序直接实现,批次间设检查点

你选哪种?