dsh-novel-generator 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +3 -1
  2. package/index.js +44 -5
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,12 +51,14 @@ dsh plugin --profile web add dsh-novel-generator
51
51
  ├── 第01章 章节名.md 章节正文(纯正文)
52
52
  ├── 章节大纲/
53
53
  │ └── 第01章 章节名.md 该章梗概 + 时间跨度 + 字数上下限 + 实际字数
54
+ │ └── .history/ 梗概被改短的旧版本(每章最多留 5 份)
54
55
  └── 世界快照/
55
- └── 第030天.md 世界观 / 角色 / 势力 / 关系 / 阶段变化
56
+ └── 第030天.md 世界观 / 角色 / 势力 / 关系 / 物品 / 事件 / 阶段变化
56
57
  ```
57
58
 
58
59
  - 每次保存正文、生成章节、保存大纲(改标题)、结算快照都会同步刷新这些文件;改标题会顺手删掉旧文件名。
59
60
  - 若你在编辑器里直接改章节正文 `.md` 且比插件记录的更新时间更晚,下次打开作品会以文件内容为准(梗概与快照目前是单向导出)。
61
+ - **梗概是自由文本,可以自带 Markdown 小标题**:大纲卡片里 `## 故事梗概` 之后直到文件结尾都算梗概正文。若某次保存把梗概改短了,旧版本会先落到 `章节大纲/.history/`,随时可以翻回来。
60
62
  - **作品名称就是文件夹名,所以受文件名规则限制**:不能为空、最多 60 字、不能以空格或点开头结尾、不能含 `\ / : * ? " < > |` 与控制字符、不能用 `CON`/`NUL`/`COM1` 等系统保留名。创建界面会即时校验并禁用「创建」按钮。
61
63
  - HTTP 接口前缀:`/dsh-novel-generator/api`,只接受本机页面同源请求。
62
64
  - 生成、推演、快照都是异步任务:`POST /generate`、`POST /compose`、`POST /deduce`、`POST /snapshot/refresh` 返回 `jobId`,再由 `GET /job?id=` 轮询。
package/index.js CHANGED
@@ -226,14 +226,49 @@ function snapshotJsonPaths(project, snapshot) {
226
226
  };
227
227
  }
228
228
 
229
- /** The synopsis body of an outline card (`(还没有写梗概)` reads as empty). */
229
+ /**
230
+ * Before an outline card is overwritten, keep the previous version when the new
231
+ * synopsis is shorter. The card is the only copy of that text, so this is the
232
+ * difference between a recoverable edit and losing thousands of characters.
233
+ * History lives in `章节大纲/.history/` and is pruned to the newest few files.
234
+ */
235
+ async function keepOutlineHistory(cardPath, nextSynopsis) {
236
+ let previous = null;
237
+ try {
238
+ previous = await readFile(cardPath, 'utf8');
239
+ } catch {
240
+ return;
241
+ }
242
+ const oldBody = parseSynopsis(previous);
243
+ if (oldBody.length <= nextSynopsis.length) return;
244
+ try {
245
+ const dir = join(join(cardPath, '..'), '.history');
246
+ await mkdir(dir, { recursive: true });
247
+ const stem = basename(cardPath, '.md');
248
+ await writeFile(join(dir, `${stem}.${Date.now()}.md`), previous, 'utf8');
249
+ const kept = (await readdir(dir)).filter((name) => name.startsWith(`${stem}.`)).sort();
250
+ for (const name of kept.slice(0, Math.max(0, kept.length - 5))) {
251
+ await unlink(join(dir, name)).catch(() => {});
252
+ }
253
+ } catch {
254
+ // History is a safety net; failing to keep it must not block the save.
255
+ }
256
+ }
257
+
258
+ /**
259
+ * The synopsis body of an outline card (`(还没有写梗概)` reads as empty).
260
+ *
261
+ * The synopsis is user prose and may itself contain Markdown headings, so it is
262
+ * everything after the marker to the end of the file — the writer always puts it
263
+ * last. Cutting at the next `## ` heading used to truncate any synopsis that had
264
+ * one inside it, and the shortened value then overwrote the file on the next
265
+ * save, which is how a 5000-character synopsis came back as a few lines.
266
+ */
230
267
  function parseSynopsis(text) {
231
268
  const marker = '## 故事梗概';
232
269
  const start = text.indexOf(marker);
233
270
  if (start === -1) return '';
234
- const rest = text.slice(start + marker.length);
235
- const next = rest.indexOf('\n## ');
236
- const body = (next === -1 ? rest : rest.slice(0, next)).trim();
271
+ const body = text.slice(start + marker.length).trim();
237
272
  return body === '(还没有写梗概)' ? '' : body;
238
273
  }
239
274
 
@@ -276,6 +311,10 @@ async function exportChapterOutline(project, chapter) {
276
311
  await mkdir(target.dir, { recursive: true });
277
312
  const outline = chapter.outline || null;
278
313
  const span = chapterWindow(project, chapter);
314
+ // The card is the only copy of the synopsis, so a write that shortens it a
315
+ // lot keeps the previous card aside first: a truncation bug then costs one
316
+ // click to undo instead of the whole text.
317
+ await keepOutlineHistory(target.file, (outline && outline.synopsis) || '');
279
318
  const lines = [];
280
319
  lines.push(`# 第 ${(Number(chapter.index) || 0) + 1} 章 ${chapter.title || '未命名'}`);
281
320
  lines.push('');
@@ -1389,7 +1428,7 @@ async function bootstrap(ctx, root) {
1389
1428
  }
1390
1429
  }
1391
1430
  return {
1392
- revision: '2026-10-21.3',
1431
+ revision: '2026-10-27.3',
1393
1432
  projects,
1394
1433
  models: {
1395
1434
  default: modelCatalog.default || null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-novel-generator",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "description": "Novel studio for DeepSeek Harness: a draggable story timeline, world snapshots, chapter outlines, and chapters written by the work's own agent.",
6
6
  "license": "MIT",