dsh-novel-generator 0.1.38 → 0.1.39

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/client.js +50 -1
  2. package/index.js +27 -16
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -718,6 +718,37 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
718
718
  }, busy ? t('整理中…', 'Merging…') : t('写入前置', 'Write into preface')))));
719
719
  }
720
720
 
721
+ /**
722
+ * After a duration change moves the chapters that follow, the snapshots stay
723
+ * where they were — this dialog is the only place the author learns about it.
724
+ */
725
+ function ShiftNoticeDialog({ shiftDays, shifted, onClose, onGoTimeline }) {
726
+ const list = Array.isArray(shifted) ? shifted : [];
727
+ const sample = list.slice(0, 6);
728
+ return h('div', { className: 'dsn-dialog-backdrop' },
729
+ h('div', { className: 'dsn-dialog', style: { width: 'min(560px, calc(100% - 48px))' } },
730
+ h('div', { className: 'dsn-dialog-head' },
731
+ h('div', { className: 'dsn-title' }, t('时间线已顺移,快照需要你手动更新', 'The timeline moved — snapshots need updating')),
732
+ h('div', { className: 'dsn-spacer' })),
733
+ h('div', { className: 'dsn-dialog-body' },
734
+ h('div', { className: 'dsn-text span4' }, t('本章的时间长度改变了 {d} 天,之后 {n} 章已整体顺移,保持首尾相接:', 'This chapter’s length changed by {d} days, and {n} following chapters moved with it so the timeline stays contiguous:', { d: shiftDays > 0 ? `+${shiftDays}` : shiftDays, n: list.length })),
735
+ list.length > 0
736
+ ? h('div', { className: 'dsn-importlist span4' },
737
+ sample.map((entry) => h('div', { key: entry.chapterId, className: 'dsn-importrow' },
738
+ h('span', { className: 'dsn-importtitle' }, entry.title || t('未命名', 'Untitled')),
739
+ h('span', { className: 'dsn-importarrow' }, '→'),
740
+ h('span', { className: 'dsn-importchars' }, t('第 {from} 天 → 第 {to} 天', 'day {from} → day {to}', { from: entry.fromDay, to: entry.toDay })))))
741
+ : null,
742
+ list.length > sample.length
743
+ ? h('div', { className: 'dsn-hint span4' }, t('另有 {n} 章同样顺移。', '{n} more chapters moved as well.', { n: list.length - sample.length }))
744
+ : null,
745
+ h('div', { className: 'dsn-hint warn span4' }, t('世界快照没有跟着移动:已有的那几份仍按旧日期保存。请到时间长河检查,把受影响的落点重新生成;如果生成正文时提示「最近的世界快照停在…」,也是去那里更新。', 'The world snapshots did not move with it — the existing ones still hold the old dates. Open the timeline, regenerate the affected points, and update them if a generation says the snapshot is behind.'))),
746
+ h('div', { className: 'dsn-dialog-foot' },
747
+ h('button', { className: 'dsn-btn ghost', type: 'button', onClick: onClose }, t('知道了', 'Got it')),
748
+ h('div', { className: 'dsn-spacer' }),
749
+ h('button', { className: 'dsn-btn primary', type: 'button', onClick: onGoTimeline }, t('去时间长河', 'Open timeline')))));
750
+ }
751
+
721
752
  function PresetDialog({ project, presets, onCancel, onSaved }) {
722
753
  const [preset, setPreset] = useState(project.agentPreset || '');
723
754
  const [busy, setBusy] = useState(false);
@@ -2212,6 +2243,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
2212
2243
  const [styleApplyJob, setStyleApplyJob] = useJob();
2213
2244
  const [styleResult, setStyleResult] = useState(null);
2214
2245
  const [styleError, setStyleError] = useState('');
2246
+ const [shiftNotice, setShiftNotice] = useState(null);
2215
2247
  // The synopsis rewrite lives here, not in OutlinePage: the page renders
2216
2248
  // inside the translated track, where an absolutely positioned dialog would
2217
2249
  // be painted off-screen.
@@ -2343,7 +2375,13 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
2343
2375
  }),
2344
2376
  'success',
2345
2377
  );
2346
- for (const note of notes) notify(note, 'error');
2378
+ // A duration change moved the chapters after it; snapshots did not move
2379
+ // with them, so this is announced in a dialog instead of a toast.
2380
+ if (result.shiftDays && Array.isArray(result.shifted) && result.shifted.length > 0) {
2381
+ setShiftNotice({ shiftDays: result.shiftDays, shifted: result.shifted });
2382
+ } else {
2383
+ for (const note of notes) notify(note, 'error');
2384
+ }
2347
2385
  onReload();
2348
2386
  } else if (summarizeJob.state === 'error') {
2349
2387
  notify(summarizeJob.error || t('同步大纲失败', 'Syncing the outline failed'), 'error');
@@ -2723,6 +2761,17 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
2723
2761
  revisingSynopsis,
2724
2762
  isFirstChapter: Boolean(chapter && chapters.length > 0 && chapter.chapterId === chapters[0].chapterId),
2725
2763
  }))),
2764
+ shiftNotice
2765
+ ? h(ShiftNoticeDialog, {
2766
+ shiftDays: shiftNotice.shiftDays,
2767
+ shifted: shiftNotice.shifted,
2768
+ onClose: () => setShiftNotice(null),
2769
+ onGoTimeline: () => {
2770
+ setShiftNotice(null);
2771
+ setView('world');
2772
+ },
2773
+ })
2774
+ : null,
2726
2775
  styleResult || applyingStyle
2727
2776
  ? h(StyleDialog, {
2728
2777
  style: styleResult ? styleResult.style : '',
package/index.js CHANGED
@@ -1499,7 +1499,7 @@ async function bootstrap(ctx, root) {
1499
1499
  }
1500
1500
  }
1501
1501
  return {
1502
- revision: '2026-10-29.1',
1502
+ revision: '2026-10-29.2',
1503
1503
  projects,
1504
1504
  models: {
1505
1505
  default: modelCatalog.default || null,
@@ -2103,7 +2103,10 @@ async function startSyncOutline(ctx, root, registry, body) {
2103
2103
  * back into its outline". Used by the story page's button and by chapter import,
2104
2104
  * so both produce identical outlines.
2105
2105
  */
2106
- async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note) {
2106
+ async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note, options = {}) {
2107
+ // Import sets the per-chapter length explicitly, so its sync keeps that number
2108
+ // and never shifts the chapters that follow.
2109
+ const keepDuration = options.keepDuration === true;
2107
2110
  const project = await readProject(root, projectId);
2108
2111
  const chapter = project.chapters.find((entry) => entry.chapterId === chapterId);
2109
2112
  if (!chapter) throw httpError(404, `章节不存在: ${chapterId}`);
@@ -2137,23 +2140,16 @@ async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note)
2137
2140
  synopsisMinChars = Math.max(20, Math.min(400, synopsisMinChars));
2138
2141
  synopsisMinChars = Math.max(1, Math.min(synopsisMinChars, synopsis.length));
2139
2142
 
2140
- // A new duration only lands when it keeps the next chapter where it is.
2143
+ // The measured duration wins, and the timeline stays contiguous: every later
2144
+ // chapter moves with it. Snapshots are deliberately left where they are — the
2145
+ // Client tells the author to regenerate the ones that no longer line up.
2141
2146
  let durationDays = chapter.durationDays;
2142
2147
  const proposed = Number.isFinite(Number(parsed.durationDays))
2143
2148
  ? Math.max(1, Math.min(365, Math.round(Number(parsed.durationDays))))
2144
2149
  : null;
2145
- if (proposed !== null && proposed !== chapter.durationDays) {
2146
- const following = (project.chapters || [])
2147
- .filter((entry) => entry.chapterId !== chapter.chapterId && entry.startDay >= span.start)
2148
- .sort((a, b) => a.startDay - b.startDay)[0] || null;
2149
- if (following === null) {
2150
- durationDays = proposed;
2151
- } else if (following.startDay === span.start + proposed) {
2152
- durationDays = proposed;
2153
- } else {
2154
- notes.push(`下一章从第 ${following.startDay} 天开始,改时长会错位,时间长度保持 ${chapter.durationDays} 天(模型建议 ${proposed} 天)`);
2155
- }
2156
- }
2150
+ const shiftDays = proposed !== null && !keepDuration ? proposed - chapter.durationDays : 0;
2151
+ if (proposed !== null && !keepDuration) durationDays = proposed;
2152
+ const shifted = [];
2157
2153
 
2158
2154
  const saved = await mutateProject(root, projectId, (draft) => {
2159
2155
  const target = draft.chapters.find((entry) => entry.chapterId === chapterId);
@@ -2169,11 +2165,24 @@ async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note)
2169
2165
  };
2170
2166
  target.durationDays = durationDays;
2171
2167
  target.updatedAt = Date.now();
2168
+ if (shiftDays !== 0) {
2169
+ for (const other of draft.chapters) {
2170
+ if (other.chapterId === target.chapterId) continue;
2171
+ if (other.startDay < span.start) continue;
2172
+ const from = other.startDay;
2173
+ other.startDay = Math.max(span.start + 1, from + shiftDays);
2174
+ other.updatedAt = Date.now();
2175
+ shifted.push({ chapterId: other.chapterId, title: other.title, fromDay: from, toDay: other.startDay });
2176
+ }
2177
+ }
2172
2178
  return { chapter: { ...target, content: undefined }, project: summarize(draft) };
2173
2179
  });
2174
2180
  const fresh = await readProject(root, projectId);
2175
2181
  await exportChapterOutline(fresh, saved.chapter);
2176
2182
  await exportProjectInfo(fresh);
2183
+ if (shifted.length > 0 && shiftDays !== 0) {
2184
+ notes.push(`本章时间长度 ${chapter.durationDays} → ${durationDays} 天,之后 ${shifted.length} 章整体顺移 ${shiftDays > 0 ? '+' : ''}${shiftDays} 天;世界快照没有跟着移动`);
2185
+ }
2177
2186
  return {
2178
2187
  synopsis,
2179
2188
  minWords,
@@ -2182,6 +2191,8 @@ async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note)
2182
2191
  synopsisMinChars,
2183
2192
  chars,
2184
2193
  notes,
2194
+ shifted,
2195
+ shiftDays,
2185
2196
  chapter: saved.chapter,
2186
2197
  };
2187
2198
  }
@@ -2421,7 +2432,7 @@ async function startImportChapters(ctx, root, registry, body) {
2421
2432
  const chapter = project.chapters.find((item) => item.chapterId === entry.chapterId);
2422
2433
  const result = await syncChapterOutline(ctx, root, projectId, entry.chapterId, chapter ? (chapter.content || '') : '', (note) => {
2423
2434
  job.message = note;
2424
- });
2435
+ }, { keepDuration: true });
2425
2436
  synced.push({ chapterId: entry.chapterId, synopsisChars: (result.synopsis || '').length });
2426
2437
  } catch (error) {
2427
2438
  failed.push({ fileName: entry.title, reason: `同步大纲失败:${error instanceof Error ? error.message : String(error)}` });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-novel-generator",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
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",