# 笔临 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 (`- [x]`) 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` - [x] **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 ``` - [x] **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')] } ``` - [x] **Step 3: global-settings defaults** 在 `defaults()` 追加 `chapterTemplates: []`, `submissionPresets: []`, `defaultSubmissionPresetId: 'builtin-webnovel'`。 - [x] **Step 4: useSettingsStore 初始字段对齐** - [x] **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` - [x] **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('') } ``` - [x] **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章 觉醒') }) ``` - [x] **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` - [x] **Step 1: TemplateQuickCreateModal** - `data-testid="chapter-quick-new"` 打开模态 - 从 `useSettingsStore` + `mergeChapterTemplates` 展示模板卡片 - 创建流程:`chapter.create` → `replaceChapterTemplate` → `plainTextToEditorHtml` → `chapter.update({ content })` - 若模板 `defaultStatus` 设置则 `chapter.update({ status })` - `switchTarget` 到新章 - [x] **Step 2: EditorLayout** 在章节面板底部保留原「+ 新章」按钮,新增「模板新建」按钮 `data-testid="chapter-quick-new"`。 - [x] **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` - [x] **Step 1: TemplateSettingsTab** - `data-testid="settings-chapter-templates"` - 内置模板只读展示 - 自定义:`setting-template-add` 新建;编辑 name/body;删除(uuid id) - 保存:`settings.update({ chapterTemplates: customOnly })` - [x] **Step 2: SettingsPage 导航** 新增 section `'templates'`,侧栏项 `settings.templates`。 - [x] **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` - [x] **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)}` } ``` - [x] **Step 2: UT-EXPORT-01 / UT-EXPORT-02** - [x] **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` - [x] **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 } } ``` - [x] **Step 2: export.handler + register + preload** ```typescript EXPORT_FORMAT_CHAPTER: 'export:formatChapter', EXPORT_COPY_CLIPBOARD: 'export:copyClipboard', EXPORT_SAVE_TXT: 'export:saveTxt', ``` - [x] **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` - [x] **Step 1: ExportModal** - `data-testid="export-modal"` - `export-preset-select` 下拉(mergeSubmissionPresets) - 预览区:调用 `export.formatChapter` 显示前 500 字 - `export-copy-clipboard` / `export-save-txt` - [x] **Step 2: SubmissionPresetTab** - `data-testid="settings-submission-presets"` - CRUD 自定义预设(不可删 builtin-webnovel) - [x] **Step 3: EditorLayout 工具栏 + App exportBook 快捷键** ```typescript if (action === 'exportBook') { if (view !== 'editor' || !chapterId) return useExportStore.getState().openModal() return } ``` 创建 `useExportStore`(open/close,类似 useGraphStore)。 - [x] **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` - [x] **Step 1: App.tsx 移除 view 限制** ```typescript if (action === 'captureInspiration') { useAppStore.getState().setInspirationModalOpen(true) return } ``` - [x] **Step 2: InspirationModal** - 无 `bookId` 时显示 `inspiration-book-select`(`useBookStore.books` 按 `lastOpenedAt` 排序) - `handleSave` 删除 `switchTarget` / `setSidebarPanel`;改为 `showToast(t('inspiration.saved'))` - 添加 `inspiration-modal--capture` class - [x] **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` - [x] **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') 分行匹配 } ``` - [x] **Step 2: UT-IMPORT-01** — 样本文本 3 章标题 - [x] **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` - [x] **Step 1: 安装依赖** ```bash npm install mammoth marked --legacy-peer-deps npm install -D @types/marked --legacy-peer-deps ``` - [x] **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 } } } ``` - [x] **Step 3: import.handler** - `IMPORT_PICK_FILE` / `IMPORT_PREVIEW` / `IMPORT_EXECUTE` - progress:`BrowserWindow.getAllWindows()[0]?.webContents.send(IPC.IMPORT_PROGRESS, { current, total })` - [x] **Step 4: IT-IMPORT-01** - [x] **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) - [x] **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 - [x] **Step 2: HomePage 替换 comingSoon** ```typescript onClick={() => setImportOpen(true)} ``` - [x] **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`(关于页版本) - [x] **Step 1: i18n 键**(spec §8 全部) - [x] **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; } ``` - [x] **Step 3: E2E** ```typescript // E2E-TEMPL-01: 模板新建章 → ProseMirror 含模板正文 // E2E-IMPORT-01: 导入 fixture txt → 多章节可见 // E2E-EXPORT-01: 导出复制 → 剪贴板含 # 第 // E2E-INSP-01: 主页 Ctrl+Shift+I → toast,仍在 home ``` - [x] **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 ``` - [x] **Step 5: 版本 1.1.0** `package.json` / `README.md` / Settings 关于页 → `v1.1.0` - [x] **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