# Novel Studio · dsh-novel-generator

[中文](README.md) · **English**

A resident plugin for DeepSeek Harness: write a novel along a timeline. Every work binds to an ordinary session, and both the chapter prose and the world snapshots are generated by the agent inside that session. Everything is stored as Markdown in your own working directory.

## Screenshots

| | |
|---|---|
| ![Work list](screenshots/1-list.png) | ![Timeline](screenshots/2-timeline.png) |
| Work list: each row has model / preset / preface / settings / prompts / import / new session / move / delete | Story timeline and world snapshots |
| ![Chapter](screenshots/3-chapter.png) | ![Outline](screenshots/4-outline.png) |
| Chapter page: sync outline, extract writing style, AI revise | Outline page: synopsis, word range, duration, AI revise synopsis |

The four images are also declared in `screenshots.json`, which is the order the plugin market detail view uses.

## What it does

| Where | What you can do |
|---|---|
| Work list | Create / open / delete works; per work: model, preset, preface prompt, settings, import chapters, new session, move, delete |
| Timeline | Scrub to any moment to see world, characters, factions, relations, items, events; set the snapshot interval; generate / regenerate / reset snapshots; per-chapter day-count chart |
| Chapter page | Edit and save prose; **sync outline**; **extract writing style**; AI revise text |
| Outline page | Title, duration, word range, synopsis and synopsis floor; **auto-deduce**; generate the chapter; **AI revise synopsis** |
| Work settings | How much previous text to carry (full / second half / none) |
| Prompt panel | Read-only view of the 9 prompt modules the plugin sends |

## Install

```sh
dsh plugin --profile web add dsh-novel-generator
# or install straight from the repo:
dsh plugin --profile web add https://github.com/XiaoOu2024/dsh-novel-generator
```

You can also install it from **Settings → Plugins**. Reload the page afterwards: a **Novel Studio** button appears at the bottom-left.

Requires **DeepSeek Harness 0.2.1-rc.2 or a newer 0.2.x**: the plugin calls `sessionController` (create / selectModel / prompt / resolveAgent), `workspaceRegistry` (create / resolveByPath / attachSession) and `agentPresets`, none of which were stable before 0.2.1. Declared as `peerDependencies: { "@deepseek-ai/dsh": "^0.2.1-rc.2" }` (optional).

## Opening it

Use the **Novel Studio** button at the bottom-left of the UI (next to settings). "Close" at the top-right leaves the panel.

## The flow

1. **Create a work**: pick a working directory (through the system directory picker — it cannot be typed), a model and a preset. Creating the work also calls `sessionController.create`, so the work owns one session; that session shows up in the sidebar as usual and you can chat in it directly.
2. **Write the outline**: open the work → timeline → "Outline": fill in the synopsis, word range and story duration. You can create an empty chapter, or let auto-deduction produce the synopsis, title and full chapter in one go.
3. **Generate the prose**: once the outline is saved, press "Generate chapter".
4. **Settle the snapshot**: world snapshots are produced automatically as story time crosses grid points; you can also generate, regenerate or reset them by hand.

**Unsaved changes are protected**: any way of leaving a chapter (back to timeline, next/previous chapter, into the outline, back to the list, closing the studio) asks first, offering "leave without saving" or "save and leave". An incomplete outline is refused with a reason.

## How world snapshots work

- Story time is counted in **days** (day 0 = the day the work was created). The **snapshot interval** (7 / 15 / 30 / 90 / 180 / 365 days, or any number you type) defines a **grid**: day 15, 30, 45…
- **Manual**: drag the timeline to a grid point and a "Generate world snapshot · day N" button appears; if that point already has one it becomes "Regenerate" and replaces it. Both ask for confirmation first.
- **Automatic**: any advance of story time onto a new grid point triggers it — creating a chapter (even an outline-only empty one), auto-deduction, generating prose, saving prose by hand. The check is idempotent: an existing point is never re-created by an ordinary save.
- **Which chapters a point covers** (half-open `[previous snapshot day, this point)`):
  - only chapters that **end strictly before** the point — the chapter ending exactly on it is the one the snapshot is being taken for, and it is still unwritten when the snapshot is created;
  - nothing already read by the previous snapshot, so **every chapter is read by exactly one snapshot**.
  - Example: interval 30 with 10-day chapters → the day-30 snapshot reads chapters 1 and 2; the day-60 snapshot reads chapters 3, 4 and 5.
- **Empty stretches cost no model call**: a point with no chapter to read (e.g. one chapter covering the whole interval) simply carries the previous snapshot forward with a note. That saves a call and, more importantly, stops the model from inventing world state that would then leak into every later chapter.
- One snapshot job per work at a time.
- **Reset**: "Reset snapshots" on the timeline clears every snapshot and rebuilds the grid from the first point to the end of the story.
- **Items and events**: snapshots carry items (name + whereabouts/use) and events (story day + title + one line), newest first.
- **What a generation may read**: the nearest snapshot before the chapter. If "chapter start − snapshot day > interval", the snapshot is judged stale and the request is refused with a pointer to update it; otherwise auto-deduction gets the snapshot plus the synopses from the snapshot day to the previous chapter, and writing adds the "chapter synopsis" on top.

## Chapter page

- Edit and save prose directly. The toolbar also has three buttons:
  - **Sync outline**: hands the prose to the model and **re-measures the whole outline from it** — synopsis, story duration, minimum words, maximum words and synopsis floor — then saves over the old outline (the text is untouched).
    - The word range always brackets what was actually written, and the synopsis floor never exceeds the new synopsis.
  - **Extract writing style**: the model reads the chapter and describes its viewpoint and person, sentence shape, wording, rhythm and description mix as bullet points (≤600 characters).
    - The studio is locked while it runs; below 300 characters the button is disabled and the endpoint returns 400.
    - The result panel is read-only, with "Copy" and "Write into preface". **Writing into the preface confirms first and says it will use the model**: the style is merged with your existing preface into one set of rules (≤2000 characters).
  - **AI revise**: give a note and the model rewrites the chapter; the result only lands in the editor and is never saved for you.
- With no prose, both buttons are disabled and the endpoints return 400; an empty note is refused too.

## Timeline shifting (important)

**Any change to a chapter's story duration moves every later chapter by the same amount**, so the timeline stays contiguous. It is triggered by:

- editing the duration in the outline page and saving;
- syncing the outline (the measured duration is adopted);
- importing chapters with "sync outlines" ticked;
- re-running auto-deduction on an existing chapter.

A dialog then lists each chapter's new position and warns that **world snapshots do not move with it** — regenerate the affected points on the timeline.

## Outline page

- **Auto-deduce**: asks "this overwrites everything" first, then regenerates synopsis, title and the whole chapter from the current word range and duration, and saves. Disabled for a work's first chapter (no snapshot, no previous chapter to build on).
- **Generate chapter**: asks "generate from the current synopsis?" first, then rewrites the prose.
- **AI revise synopsis**: rewrites only the synopsis, from the current one plus your note; the text and title stay. The result fills the synopsis box unsaved.

> The deliberate difference: "Sync outline" saves over the outline (it reads prose you have already settled), while "AI revise synopsis" only produces a draft (it changes the material you are about to generate from).

## Previous-text reference (work settings)

"Settings" on a work row controls how much previous text **auto-deduction** and **chapter generation** carry:

| Option | What the prompt gets |
|---|---|
| **Previous chapter (full)** (default) | The whole previous chapter, to keep voice, scene and the last line continuous |
| Previous chapter (second half) | From the sentence containing the midpoint to the end |
| No prose | Only the world snapshot and chapter synopses |

- A previous chapter longer than 8000 characters is trimmed to its **last 8000** (continuity lives at the end).
- "Previous chapter" means the closest chapter starting before this one that has prose; with no prose the block is omitted.
- The block sits after "synopses of what already happened" and before "this chapter's synopsis".
- Outline sync, style extraction and world snapshots carry **no** previous prose — those are measurement/structured tasks where it only gets in the way.

## Preface prompt (per work)

"Preface" on a work row sets one book's standing constraints: built-in「第三人称全知（默认）」/ 第三人称限知 / 第一人称 / 自定义, select to fill the box, editable freely (≤2000 characters). **New works start with the 6-point third-person omniscient text**, which you can change or clear.

- It is sent as "【本书前置设定】…" at the top of chapter writing, auto-deduction, AI revise (text and synopsis), style extraction and preface merging; world snapshots and outline sync do not carry it.
- Saving takes effect on the next generation — no need to rebuild the session.
- It is also written into `作品信息.md` so you can read and back it up outside the UI. Empty means "not set".

## Importing chapters

"Import" on a work row pulls ready-made `.md` chapters in: pick a folder → review the match → confirm.

- Recognised numbering: `第1章`, `第007章`, `第十二章`; the suffix may also be `回` / `节`; extensions `.md` / `.markdown` / `.txt`.
- Matching walks `1, 2, 3…` (or "last chapter number + 1") from the chosen start and **stops at the first gap**; anything after a gap is not imported.
- Starting from chapter 1 skips numbers the work already uses (no overwrite, no duplicates).
- The title is whatever follows the number (`第3章.md` has none, so it becomes "第 3 章").
- The preview lists every match (`file → chapter N · title · length`), any skipped numbers, **files that did not connect**, **files with no chapter number**, and encoding-suspicious marks.
- After importing, prose is written into the work and exported as `第NN章 标题.md`; with "sync outlines" ticked each chapter gets one model call (with progress and a stop button); crossing grid points triggers world snapshots. The per-chapter days you type are the starting value — syncing then adopts the measured day count and shifts later chapters.

## Prompt panel (read-only)

"Prompts" on a work row opens a read-only panel listing the **9 prompt modules** the plugin sends, each with when it fires, and a copy button:

| Module | Fires when |
|---|---|
| Book preface block | chapter writing / auto-deduction / AI revise text / AI revise synopsis / style extraction / preface merging |
| Write chapter | "Generate chapter", or the writing step of auto-deduction |
| Auto-deduce outline | "Auto-deduce" |
| World snapshot | "Generate snapshot", or automatically |
| Sync outline | "Sync outline", or import with syncing |
| AI revise text / synopsis | the two "AI revise" buttons |
| Extract style / merge into preface | the two style buttons |

- **Read-only; live data appears as `｛…｝`**: the panel never reads or echoes your work's content.
- Rendering: the Host runs the **real prompt builders** against placeholder data (a fabricated chapter and snapshot), so the structure matches what is actually sent, line for line, and cannot drift as the code evolves.
- The panel follows the work's state: with no snapshot yet it shows "（暂无世界快照…）" and the "from the start" heading; with one it starts from that snapshot's day.
- The footer summarises the current settings (previous-text reference, whether a preface is set, snapshot interval, latest snapshot day).

## The bundled "Novel Studio · Author" preset

The plugin depends on `dsh-novel-author-preset`, so `cordis.patch.yml` declares **two rows**:

| Row in the plugin list | What it is |
|---|---|
| `dsh-novel-generator` (Novel Studio) | The studio itself: timeline, chapters, snapshots, preface |
| `dsh-novel-author-preset` (Novel Studio · Author preset) | An agent preset: registered on activation, unregistered when switched off |

- Both rows sit under **Novel Studio** and can be switched independently: turning the preset row off only hides it from the preset list, the studio keeps working; turning the studio off removes the preset with it.
- The preset row shows the companion package's **own title and description**, not the generic "Agent 预设定义" of `@deepseek-ai/dsh-agent-preset`.
- With it installed, **new works default to this preset**; if it is switched off or missing, rebuilding a session falls back to the profile default instead of failing.

The preset is deliberately thin: three sentences of persona (you are a novelist / output prose only / style, viewpoint and length follow the request), no runtime context, **no tools at all**, and context compaction only. All literary direction belongs to the work-level preface.

## Where things are stored

- **Index**: `<DSH_HOME>/novel-studio/projects/<project id>/project.json` (`DSH_HOME` defaults to `~/.dsh`). Timeline, outlines and snapshots live here; `dataDir` moves it.
- **Prose, synopses and snapshots are written into a folder named after the work, inside your working directory**:

  ```
  <working dir>/<work title>/
  ├── 作品信息.md                  parameters + chapter table + preface prompt
  ├── 第01章 章节名.md             chapter prose (prose only)
  ├── 章节大纲/
  │   ├── 第01章 章节名.md         synopsis + span + word range + actual length
  │   └── .history/               older synopses replaced by shorter ones (5 per chapter)
  └── 世界快照/
      └── 第030天.md               world / characters / factions / relations / items / events
  ```

  - Saving prose, generating a chapter, saving an outline and settling a snapshot all refresh these files; renaming a chapter removes the old file.
  - If you edit a chapter `.md` outside the plugin and it is newer than the recorded update, the file wins on the next open (synopses and snapshots are exported one way).
  - Synopses are free text and may carry Markdown headings; everything after `## 故事梗概` counts as the synopsis.
- **The work title is the folder name**, so it follows file-name rules: non-empty, ≤60 characters, no leading/trailing space or dot, none of `\ / : * ? " < > |` or control characters, and not a reserved name like `CON` / `NUL` / `COM1`. The create form validates as you type.
- **HTTP API** prefix `/dsh-novel-generator/api`, same-origin local page requests only.
- **Async jobs**: `POST /generate`, `/compose`, `/deduce`, `/snapshot/refresh`, `/chapter/sync-outline`, `/style/extract`, `/style/apply`, `/import/apply` return a `jobId`; poll `GET /job?id=`, cancel with `/job/cancel`.
- `timeline.cursor` is measured in days; older grid-based data is converted once on first read and marked `cursorUnit: "day"`.

## Moving, importing and sessions

- **Change preset**: this sets the preset for the *next* session; press "New session" on the same row to apply it immediately.
- **Move**: relocates the whole work folder and updates `cwd`; refused when the target already has a non-empty folder of that name.
- **Import an existing work**: "Import existing work…" above the list takes a folder containing `作品信息.md` and rebuilds the entry from the files (chapters, outlines, synopses, lengths, durations, snapshots), then binds a fresh session. It doubles as a recovery path if `~/.dsh/novel-studio` is lost.
- **Sessions**: one per work. A deleted session is rebuilt automatically; an **archived** one cannot be detected (DSH exposes no query), so use "New session" to replace it. Before each task the session's model is aligned with the model recorded on the work.
- **Sidebar groups**: DSH groups sessions by directory, so works in different directories each get a "小说工坊 · <dir>" group; empty and leftover groups are cleaned up.

## Localisation

The UI follows DSH **Settings → General → Language** (the client uses `ctx.locale` and `useTranslator()`). The sidebar entry, studio title bar and loading messages already switch; the rest of the copy is still Chinese, to be translated one string at a time.

## Self-check scripts (development only, not published)

```bash
node selfcheck.mjs <temp dir>          # create / timeline / chapters / outline / save / delete
node selfcheck-generate.mjs <temp dir> # one real model call to write a chapter
node selfcheck-snapshot.mjs <temp dir> # one real model call to produce a snapshot
node selfcheck-compose.mjs <temp dir>  # auto-deduce a whole chapter and check snapshot triggering
```

They talk to `http://127.0.0.1:3080` and are meant for local verification only.

## Marketplace entry

The `awesome-dsh-plugin` entry lives at `data/plugins/XiaoOu2024__dsh-novel-generator.yml`:

```yaml
url: https://github.com/XiaoOu2024/dsh-novel-generator
name: XiaoOu2024/dsh-novel-generator
category: ui
description:
  en: 'Novel generator: builds a world snapshot along a story timeline and, from the latest snapshot and the most recent chapters, deduces the next chapter.'
  zh: '小说生成器：按时间线生成世界快照，通过世界快照和最近章节自动推演新章节。'
```

Requirements before submitting: the repo is pushed, at least one day old, and tagged with the `dsh-plugin` topic.

## License

MIT
