# 笔临 v1.1 作家日更工具包 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:** 交付 v1.1.0:§10 章节模板 + 快速新建、§11.1 txt/md/docx 导入、§20.2 投稿预设单章导出、§20.6 全局灵感捕获增强。
**Architecture:** `GlobalSettings` 扩展 `chapterTemplates` / `submissionPresets`;渲染进程 `chapter-template.ts` 做变量替换后复用 `chapter.create/update`;主进程 `ImportService`(mammoth/marked + `chapter-splitter`)与 `ExportService`(`submission-formatter` + clipboard);灵感模态解除视图限制且保存不跳转。
**Tech Stack:** Electron 36、electron-vite、React 18、TypeScript、node:sqlite、mammoth、marked、Zustand、i18next、Vitest、Playwright
**Spec:** `docs/superpowers/specs/2026-07-08-bilin-v11-writer-toolkit-design.md`
---
## File Map
| 文件 | 职责 |
|------|------|
| `src/shared/types.ts` | `ChapterTemplate`、`SubmissionPreset`、`Import*`、`Export*` DTO |
| `src/shared/builtin-templates.ts` | 内置模板与投稿预设常量 |
| `src/shared/ipc-channels.ts` | `import.*` / `export.*` / `IMPORT_PROGRESS` |
| `src/main/lib/chapter-splitter.ts` | txt/md/html 分章 |
| `src/main/lib/submission-formatter.ts` | HTML → 投稿纯文本 |
| `src/main/services/import.service.ts` | 读文件、预览、创建书+章 |
| `src/main/services/export.service.ts` | 格式化章节、剪贴板、存盘 |
| `src/main/ipc/handlers/import.handler.ts` | 导入 IPC + progress 事件 |
| `src/main/ipc/handlers/export.handler.ts` | 导出 IPC |
| `src/main/ipc/register.ts` | 注册 import/export |
| `src/preload/index.ts` + `electron-api.d.ts` | import / export API |
| `src/renderer/lib/chapter-template.ts` | 模板变量替换 |
| `src/renderer/components/chapter/TemplateQuickCreateModal.tsx` | 快速新建章 |
| `src/renderer/components/settings/TemplateSettingsTab.tsx` | 模板 CRUD |
| `src/renderer/components/settings/SubmissionPresetTab.tsx` | 投稿预设 CRUD |
| `src/renderer/components/import/ImportBookModal.tsx` | 导入 UI |
| `src/renderer/components/export/ExportModal.tsx` | 导出 UI |
| `src/renderer/components/inspiration/InspirationModal.tsx` | 全局灵感增强 |
| `src/renderer/components/home/HomePage.tsx` | 导入入口 |
| `src/renderer/components/layout/EditorLayout.tsx` | 快速新建/导出入口 |
| `src/renderer/App.tsx` | 全局灵感快捷键 |
| `src/renderer/stores/useSettingsStore.ts` | 新 settings 字段默认值 |
| `src/main/services/global-settings.ts` | defaults 合并 |
| `public/locales/*/translation.json` | template / import / export / submission / inspiration |
| `src/renderer/styles/layout.css` | 模板/导入/导出样式 |
| `tests/main/chapter-template.test.ts` | UT-TEMPL-* |
| `tests/main/chapter-splitter.test.ts` | UT-IMPORT-01 |
| `tests/main/submission-formatter.test.ts` | UT-EXPORT-* |
| `tests/main/import.service.test.ts` | IT-IMPORT-01 |
| `e2e/chapter-template.spec.ts` | E2E-TEMPL-01 |
| `e2e/import-book.spec.ts` | E2E-IMPORT-01 |
| `e2e/export-submission.spec.ts` | E2E-EXPORT-01 |
| `e2e/inspiration-capture.spec.ts` | E2E-INSP-01 |
---
## Task 1: 类型 + 内置常量 + GlobalSettings
**Files:**
- Modify: `src/shared/types.ts`
- Create: `src/shared/builtin-templates.ts`
- Modify: `src/main/services/global-settings.ts`
- Modify: `src/renderer/stores/useSettingsStore.ts`
- [ ] **Step 1: 扩展 types**
```typescript
export interface ChapterTemplate {
id: string
name: string
body: string
defaultStatus?: ChapterStatus
builtin?: boolean
}
export interface SubmissionPreset {
id: string
name: string
titleFormat: string
indentParagraphs: boolean
includeAuthorsNote: boolean
paragraphGap: 'single' | 'double'
}
export type ImportSplitMode = 'auto' | 'paragraph' | 'regex'
export interface ImportChapterPreview {
title: string
contentHtml: string
wordCount: number
}
export interface ImportPreviewResult {
fileName: string
fileSizeBytes: number
chapters: ImportChapterPreview[]
warnings: string[]
}
export interface ImportExecuteParams {
filePath: string
bookName: string
category: string
splitMode: ImportSplitMode
customRegex?: string
}
// GlobalSettings 追加:
chapterTemplates?: ChapterTemplate[]
submissionPresets?: SubmissionPreset[]
defaultSubmissionPresetId?: string
```
- [ ] **Step 2: builtin-templates.ts**
```typescript
import type { ChapterTemplate, SubmissionPreset } from './types'
export const BUILTIN_CHAPTER_TEMPLATES: ChapterTemplate[] = [
{
id: 'builtin-generic',
name: '通用模板',
body: '## 第{chapterNumber}章 {chapterTitle}\n\n',
builtin: true
},
{
id: 'builtin-webnovel',
name: '网文常用',
body: '## 第{chapterNumber}章 {chapterTitle}\n\n\n\n——\n作者的话:\n',
builtin: true
}
]
export const BUILTIN_SUBMISSION_PRESETS: SubmissionPreset[] = [
{
id: 'builtin-webnovel',
name: '通用网文',
titleFormat: '# 第{chapterNumber}章 {chapterTitle}',
indentParagraphs: true,
includeAuthorsNote: true,
paragraphGap: 'double'
}
]
export function mergeChapterTemplates(custom: ChapterTemplate[] = []): ChapterTemplate[] {
return [...BUILTIN_CHAPTER_TEMPLATES, ...custom.filter((t) => !t.builtin)]
}
export function mergeSubmissionPresets(custom: SubmissionPreset[] = []): SubmissionPreset[] {
return [...BUILTIN_SUBMISSION_PRESETS, ...custom.filter((p) => p.id !== 'builtin-webnovel')]
}
```
- [ ] **Step 3: global-settings defaults**
在 `defaults()` 追加 `chapterTemplates: []`, `submissionPresets: []`, `defaultSubmissionPresetId: 'builtin-webnovel'`。
- [ ] **Step 4: useSettingsStore 初始字段对齐**
- [ ] **Step 5: build**
```bash
npm run build
```
---
## Task 2: chapter-template 引擎 + 单元测试
**Files:**
- Create: `src/renderer/lib/chapter-template.ts`
- Create: `tests/main/chapter-template.test.ts`
- [ ] **Step 1: 实现 replaceChapterTemplate**
```typescript
export interface TemplateContext {
chapterNumber: number
chapterTitle: string
penName: string
date: string
volumeName: string
previousChapterTitle: string
outlineItem: string
characterList: string
}
const VARS = [
'chapterNumber', 'chapterTitle', 'penName', 'date',
'volumeName', 'previousChapterTitle', 'outlineItem', 'characterList'
] as const
export function replaceChapterTemplate(body: string, ctx: TemplateContext): string {
let out = body
for (const key of VARS) {
out = out.split(`{${key}}`).join(String(ctx[key]))
}
return out
}
export function plainTextToEditorHtml(text: string): string {
return text
.split(/\n{2,}/)
.map((p) => `
${p.replace(/\n/g, '
')}
`)
.join('')
}
```
- [ ] **Step 2: UT-TEMPL-01~03**
```typescript
it('UT-TEMPL-01: replaces chapterNumber and chapterTitle', () => {
const result = replaceChapterTemplate('第{chapterNumber}章 {chapterTitle}', {
chapterNumber: 5, chapterTitle: '觉醒',
penName: '', date: '', volumeName: '', previousChapterTitle: '',
outlineItem: '', characterList: ''
})
expect(result).toBe('第5章 觉醒')
})
```
- [ ] **Step 3: 运行**
```bash
npm run test -- tests/main/chapter-template.test.ts
```
---
## Task 3: TemplateQuickCreateModal + 章节侧栏入口
**Files:**
- Create: `src/renderer/components/chapter/TemplateQuickCreateModal.tsx`
- Modify: `src/renderer/components/layout/EditorLayout.tsx`
- [ ] **Step 1: TemplateQuickCreateModal**
- `data-testid="chapter-quick-new"` 打开模态
- 从 `useSettingsStore` + `mergeChapterTemplates` 展示模板卡片
- 创建流程:`chapter.create` → `replaceChapterTemplate` → `plainTextToEditorHtml` → `chapter.update({ content })`
- 若模板 `defaultStatus` 设置则 `chapter.update({ status })`
- `switchTarget` 到新章
- [ ] **Step 2: EditorLayout**
在章节面板底部保留原「+ 新章」按钮,新增「模板新建」按钮 `data-testid="chapter-quick-new"`。
- [ ] **Step 3: build**
```bash
npm run build
```
---
## Task 4: 设置页章节模板 Tab
**Files:**
- Create: `src/renderer/components/settings/TemplateSettingsTab.tsx`
- Modify: `src/renderer/components/settings/SettingsPage.tsx`
- [ ] **Step 1: TemplateSettingsTab**
- `data-testid="settings-chapter-templates"`
- 内置模板只读展示
- 自定义:`setting-template-add` 新建;编辑 name/body;删除(uuid id)
- 保存:`settings.update({ chapterTemplates: customOnly })`
- [ ] **Step 2: SettingsPage 导航**
新增 section `'templates'`,侧栏项 `settings.templates`。
- [ ] **Step 3: build**
```bash
npm run build
```
---
## Task 5: submission-formatter + 单元测试
**Files:**
- Create: `src/main/lib/submission-formatter.ts`
- Create: `tests/main/submission-formatter.test.ts`
- [ ] **Step 1: 实现**
```typescript
import { stripHtml } from '../services/word-count'
import type { SubmissionPreset } from '../../shared/types'
export interface FormatChapterInput {
chapterNumber: number
chapterTitle: string
contentHtml: string
preset: SubmissionPreset
}
export function formatChapterForSubmission(input: FormatChapterInput): string {
let body = stripHtml(input.contentHtml)
if (!input.preset.includeAuthorsNote) {
body = body.split(/\n——\n/)[0] ?? body
}
const paragraphs = body.split(/\n+/).filter((p) => p.trim())
const indented = input.preset.indentParagraphs
? paragraphs.map((p) => ` ${p.trim()}`)
: paragraphs.map((p) => p.trim())
const gap = input.preset.paragraphGap === 'double' ? '\n\n' : '\n'
const title = input.preset.titleFormat
.replace('{chapterNumber}', String(input.chapterNumber))
.replace('{chapterTitle}', input.chapterTitle)
return `${title}\n\n${indented.join(gap)}`
}
```
- [ ] **Step 2: UT-EXPORT-01 / UT-EXPORT-02**
- [ ] **Step 3: 运行**
```bash
npm run test -- tests/main/submission-formatter.test.ts
```
---
## Task 6: ExportService + IPC + preload
**Files:**
- Create: `src/main/services/export.service.ts`
- Create: `src/main/ipc/handlers/export.handler.ts`
- Modify: `src/shared/ipc-channels.ts`
- Modify: `src/main/ipc/register.ts`
- Modify: `src/preload/index.ts`
- Modify: `src/shared/electron-api.d.ts`
- [ ] **Step 1: ExportService**
```typescript
export class ExportService {
constructor(
private registry: BookRegistryService,
private settings: GlobalSettingsService
) {}
formatChapter(bookId: string, chapterId: string, presetId: string): string {
const db = this.registry.getDb(bookId)
const chapter = new ChapterRepository(db).get(chapterId)
if (!chapter) throw new Error('Chapter not found')
const presets = mergeSubmissionPresets(this.settings.get().submissionPresets)
const preset = presets.find((p) => p.id === presetId) ?? BUILTIN_SUBMISSION_PRESETS[0]
const chapters = new ChapterRepository(db).list().filter((c) => c.volumeId === chapter.volumeId)
const chapterNumber = chapters.findIndex((c) => c.id === chapterId) + 1
return formatChapterForSubmission({
chapterNumber, chapterTitle: chapter.title,
contentHtml: chapter.content, preset
})
}
copyToClipboard(text: string): void {
clipboard.writeText(text)
}
async saveTxt(text: string, defaultName: string): Promise {
const { canceled, filePath } = await dialog.showSaveDialog({
defaultPath: defaultName,
filters: [{ name: 'Text', extensions: ['txt'] }]
})
if (canceled || !filePath) return null
writeFileSync(filePath, text, 'utf8')
return filePath
}
}
```
- [ ] **Step 2: export.handler + register + preload**
```typescript
EXPORT_FORMAT_CHAPTER: 'export:formatChapter',
EXPORT_COPY_CLIPBOARD: 'export:copyClipboard',
EXPORT_SAVE_TXT: 'export:saveTxt',
```
- [ ] **Step 3: build**
```bash
npm run build
```
---
## Task 7: ExportModal + 投稿预设设置 Tab + 快捷键
**Files:**
- Create: `src/renderer/components/export/ExportModal.tsx`
- Create: `src/renderer/components/settings/SubmissionPresetTab.tsx`
- Modify: `src/renderer/components/settings/SettingsPage.tsx`
- Modify: `src/renderer/components/layout/EditorLayout.tsx`
- Modify: `src/renderer/App.tsx`
- [ ] **Step 1: ExportModal**
- `data-testid="export-modal"`
- `export-preset-select` 下拉(mergeSubmissionPresets)
- 预览区:调用 `export.formatChapter` 显示前 500 字
- `export-copy-clipboard` / `export-save-txt`
- [ ] **Step 2: SubmissionPresetTab**
- `data-testid="settings-submission-presets"`
- CRUD 自定义预设(不可删 builtin-webnovel)
- [ ] **Step 3: EditorLayout 工具栏 + App exportBook 快捷键**
```typescript
if (action === 'exportBook') {
if (view !== 'editor' || !chapterId) return
useExportStore.getState().openModal()
return
}
```
创建 `useExportStore`(open/close,类似 useGraphStore)。
- [ ] **Step 4: build**
```bash
npm run build
```
---
## Task 8: InspirationModal 全局增强
**Files:**
- Modify: `src/renderer/components/inspiration/InspirationModal.tsx`
- Modify: `src/renderer/App.tsx`
- Modify: `src/renderer/styles/layout.css`
- [ ] **Step 1: App.tsx 移除 view 限制**
```typescript
if (action === 'captureInspiration') {
useAppStore.getState().setInspirationModalOpen(true)
return
}
```
- [ ] **Step 2: InspirationModal**
- 无 `bookId` 时显示 `inspiration-book-select`(`useBookStore.books` 按 `lastOpenedAt` 排序)
- `handleSave` 删除 `switchTarget` / `setSidebarPanel`;改为 `showToast(t('inspiration.saved'))`
- 添加 `inspiration-modal--capture` class
- [ ] **Step 3: E2E 预备 build**
```bash
npm run build
```
---
## Task 9: chapter-splitter + 单元测试
**Files:**
- Create: `src/main/lib/chapter-splitter.ts`
- Create: `tests/main/chapter-splitter.test.ts`
- [ ] **Step 1: splitPlainText**
```typescript
export function splitPlainText(
text: string,
mode: ImportSplitMode,
customRegex?: string
): Array<{ title: string; body: string }> {
// auto: 尝试 MARKDOWN_HEADING 再 CHINESE_CHAPTER
// paragraph: split \n\s*\n
// regex: new RegExp(customRegex, 'gm') 分行匹配
}
```
- [ ] **Step 2: UT-IMPORT-01** — 样本文本 3 章标题
- [ ] **Step 3: 运行**
```bash
npm run test -- tests/main/chapter-splitter.test.ts
```
---
## Task 10: ImportService + IPC + 依赖安装
**Files:**
- Create: `src/main/services/import.service.ts`
- Create: `src/main/ipc/handlers/import.handler.ts`
- Modify: `package.json`
- Modify: `src/main/ipc/register.ts`
- Modify: `src/preload/index.ts`
- Create: `tests/main/import.service.test.ts`
- [ ] **Step 1: 安装依赖**
```bash
npm install mammoth marked --legacy-peer-deps
npm install -D @types/marked --legacy-peer-deps
```
- [ ] **Step 2: ImportService**
```typescript
export class ImportService {
async pickFile(): Promise<{ canceled: boolean; filePath?: string }> { /* dialog */ }
async preview(filePath: string, splitMode: ImportSplitMode, customRegex?: string): Promise {
const stat = statSync(filePath)
const ext = extname(filePath).toLowerCase()
let html: string
if (ext === '.docx') html = (await mammoth.convertToHtml({ path: filePath })).value
else if (ext === '.md') html = marked.parse(readFileSync(filePath, 'utf8')) as string
else html = plainTextToEditorHtml(readFileSync(filePath, 'utf8'))
const plain = stripHtml(html)
const parts = splitPlainText(plain, splitMode, customRegex)
// map to ImportChapterPreview with wordCount
}
async execute(params: ImportExecuteParams, onProgress?: (c: number, t: number) => void): Promise {
const preview = await this.preview(...)
const meta = this.registry.create({ name: params.bookName, category: params.category, createSampleChapter: false })
try {
const db = this.registry.getDb(meta.id)
const volId = new VolumeRepository(db).list()[0]?.id ?? new VolumeRepository(db).create('第一卷', 0).id
for (let i = 0; i < preview.chapters.length; i++) {
const ch = preview.chapters[i]
new ChapterRepository(db).create(volId, ch.title, i, ch.contentHtml)
onProgress?.(i + 1, preview.chapters.length)
}
return meta.id
} catch (e) {
this.registry.delete(meta.id)
throw e
}
}
}
```
- [ ] **Step 3: import.handler**
- `IMPORT_PICK_FILE` / `IMPORT_PREVIEW` / `IMPORT_EXECUTE`
- progress:`BrowserWindow.getAllWindows()[0]?.webContents.send(IPC.IMPORT_PROGRESS, { current, total })`
- [ ] **Step 4: IT-IMPORT-01**
- [ ] **Step 5: 运行**
```bash
npm run test -- tests/main/import.service.test.ts
```
---
## Task 11: ImportBookModal + HomePage
**Files:**
- Create: `src/renderer/components/import/ImportBookModal.tsx`
- Modify: `src/renderer/components/home/HomePage.tsx`
- Modify: `src/renderer/App.tsx`(挂载 ImportBookModal)
- [ ] **Step 1: ImportBookModal**
- `data-testid="import-book-modal"`
- 浏览 → `import.pickFile`
- 分章规则 select + 条件 regex 输入
- 预览按钮 → `import.preview` 显示章数与前 5 标题
- `fileSizeBytes > 20 * 1024 * 1024` → confirm 对话框
- 执行 → 监听 `onImportProgress` → `import-progress-bar`
- 成功 → `openBook(bookId)` + 切 editor
- [ ] **Step 2: HomePage 替换 comingSoon**
```typescript
onClick={() => setImportOpen(true)}
```
- [ ] **Step 3: build**
```bash
npm run build
```
---
## Task 12: i18n + CSS + E2E + v1.1.0
**Files:**
- Modify: `public/locales/zh-CN/translation.json`
- Modify: `public/locales/en/translation.json`
- Modify: `src/renderer/styles/layout.css`
- Create: `e2e/chapter-template.spec.ts`
- Create: `e2e/import-book.spec.ts`
- Create: `e2e/export-submission.spec.ts`
- Create: `e2e/inspiration-capture.spec.ts`
- Modify: `package.json`
- Modify: `README.md`
- Modify: `src/renderer/components/settings/SettingsPage.tsx`(关于页版本)
- [ ] **Step 1: i18n 键**(spec §8 全部)
- [ ] **Step 2: CSS**
```css
.template-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 10px; }
.import-preview-list { font-size: 12px; max-height: 120px; overflow: auto; }
.export-preview { font-family: var(--font-mono); font-size: 11px; white-space: pre-wrap; max-height: 200px; overflow: auto; }
.inspiration-modal--capture { z-index: 10000; }
```
- [ ] **Step 3: E2E**
```typescript
// E2E-TEMPL-01: 模板新建章 → ProseMirror 含模板正文
// E2E-IMPORT-01: 导入 fixture txt → 多章节可见
// E2E-EXPORT-01: 导出复制 → 剪贴板含 # 第
// E2E-INSP-01: 主页 Ctrl+Shift+I → toast,仍在 home
```
- [ ] **Step 4: 全量验证**
```bash
npm run build
npm run test
npm run test:e2e -- e2e/chapter-template.spec.ts e2e/import-book.spec.ts e2e/export-submission.spec.ts e2e/inspiration-capture.spec.ts
```
- [ ] **Step 5: 版本 1.1.0**
`package.json` / `README.md` / Settings 关于页 → `v1.1.0`
- [ ] **Step 6: 勾选本 plan 全部 checkbox**
---
## Spec Coverage Checklist
| Spec 要求 | Task |
|-----------|------|
| chapterTemplates + 内置模板 | Task 1, 4 |
| 模板变量 + 快速新建 | Task 2, 3 |
| submissionPresets + 格式化 | Task 1, 5, 6 |
| ExportModal + 剪贴板/txt | Task 7 |
| 全局灵感 + 不跳转 | Task 8 |
| txt/md/docx 导入 + 分章 | Task 9, 10, 11 |
| >20MB 确认 + 进度条 | Task 11 |
| i18n / E2E / v1.1.0 | Task 12 |
---
## Execution Notes
- `plainTextToEditorHtml` 与 `chapter-template.ts` 可共享;主进程导入侧可 import 同逻辑或 duplicate 最小 HTML 包装
- 导入 `.docx` 用 mammoth HTML 再 `stripHtml` 分章(章标题在纯文本层匹配)
- `export.formatChapter` 与模板 `{chapterNumber}` 均用卷内 sortOrder 计算序号
- E2E 导入使用 `tests/fixtures/import-three-chapters.txt` fixture 文件
- 用户未要求时不自动 git commit;每 Task 完成后勾选 plan checkbox