Break down templates, import, submission export, and inspiration capture into twelve tracked tasks for the daily writing workflow release. Co-authored-by: Cursor <cursoragent@cursor.com>
20 KiB
笔临 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
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
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
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
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>${p.replace(/\n/g, '<br>')}</p>`)
.join('')
}
- Step 2: UT-TEMPL-01~03
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: 运行
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
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
npm run build
Task 5: submission-formatter + 单元测试
Files:
-
Create:
src/main/lib/submission-formatter.ts -
Create:
tests/main/submission-formatter.test.ts -
Step 1: 实现
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: 运行
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
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<string | null> {
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
EXPORT_FORMAT_CHAPTER: 'export:formatChapter',
EXPORT_COPY_CLIPBOARD: 'export:copyClipboard',
EXPORT_SAVE_TXT: 'export:saveTxt',
- Step 3: build
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 快捷键
if (action === 'exportBook') {
if (view !== 'editor' || !chapterId) return
useExportStore.getState().openModal()
return
}
创建 useExportStore(open/close,类似 useGraphStore)。
- Step 4: build
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 限制
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--captureclass -
Step 3: E2E 预备 build
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
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: 运行
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: 安装依赖
npm install mammoth marked --legacy-peer-deps
npm install -D @types/marked --legacy-peer-deps
- Step 2: ImportService
export class ImportService {
async pickFile(): Promise<{ canceled: boolean; filePath?: string }> { /* dialog */ }
async preview(filePath: string, splitMode: ImportSplitMode, customRegex?: string): Promise<ImportPreviewResult> {
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<string> {
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: 运行
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
onClick={() => setImportOpen(true)}
- Step 3: build
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
.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
// E2E-TEMPL-01: 模板新建章 → ProseMirror 含模板正文
// E2E-IMPORT-01: 导入 fixture txt → 多章节可见
// E2E-EXPORT-01: 导出复制 → 剪贴板含 # 第
// E2E-INSP-01: 主页 Ctrl+Shift+I → toast,仍在 home
- Step 4: 全量验证
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.txtfixture 文件 - 用户未要求时不自动 git commit;每 Task 完成后勾选 plan checkbox