dsh-novel-generator 0.1.38 → 0.1.40
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.
- package/client.js +83 -3
- package/index.js +79 -19
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -718,6 +718,39 @@ 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, scope, 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' }, scope === 'import'
|
|
735
|
+
? t('导入时同步大纲改动了章节的时间长度,之后 {n} 章已整体顺移,保持首尾相接:', 'Syncing the imported chapters changed their lengths, and {n} following chapters moved with them so the timeline stays contiguous:', { n: list.length })
|
|
736
|
+
: 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 })),
|
|
737
|
+
list.length > 0
|
|
738
|
+
? h('div', { className: 'dsn-importlist span4' },
|
|
739
|
+
sample.map((entry) => h('div', { key: entry.chapterId, className: 'dsn-importrow' },
|
|
740
|
+
h('span', { className: 'dsn-importtitle' }, entry.title || t('未命名', 'Untitled')),
|
|
741
|
+
h('span', { className: 'dsn-importarrow' }, '→'),
|
|
742
|
+
h('span', { className: 'dsn-importchars' }, t('第 {from} 天 → 第 {to} 天', 'day {from} → day {to}', { from: entry.fromDay, to: entry.toDay })))))
|
|
743
|
+
: null,
|
|
744
|
+
list.length > sample.length
|
|
745
|
+
? h('div', { className: 'dsn-hint span4' }, t('另有 {n} 章同样顺移。', '{n} more chapters moved as well.', { n: list.length - sample.length }))
|
|
746
|
+
: null,
|
|
747
|
+
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.'))),
|
|
748
|
+
h('div', { className: 'dsn-dialog-foot' },
|
|
749
|
+
h('button', { className: 'dsn-btn ghost', type: 'button', onClick: onClose }, t('知道了', 'Got it')),
|
|
750
|
+
h('div', { className: 'dsn-spacer' }),
|
|
751
|
+
h('button', { className: 'dsn-btn primary', type: 'button', onClick: onGoTimeline }, t('去时间长河', 'Open timeline')))));
|
|
752
|
+
}
|
|
753
|
+
|
|
721
754
|
function PresetDialog({ project, presets, onCancel, onSaved }) {
|
|
722
755
|
const [preset, setPreset] = useState(project.agentPreset || '');
|
|
723
756
|
const [busy, setBusy] = useState(false);
|
|
@@ -1672,7 +1705,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
1672
1705
|
}, busy ? t('改写中…', 'Revising…') : t('确认修改', 'Revise')))));
|
|
1673
1706
|
}
|
|
1674
1707
|
|
|
1675
|
-
function OutlinePage({ detail, chapter, isFirstChapter, onBack, onSaved, notify, onRegisterGuard, requestConfirm, onBusyChange, onReviseSynopsis, synopsisDraft, onSynopsisDraftApplied, revisingSynopsis }) {
|
|
1708
|
+
function OutlinePage({ detail, chapter, isFirstChapter, onBack, onSaved, notify, onRegisterGuard, requestConfirm, onBusyChange, onReviseSynopsis, synopsisDraft, onSynopsisDraftApplied, revisingSynopsis, onShiftNotice }) {
|
|
1676
1709
|
const buildForm = useCallback(() => ({
|
|
1677
1710
|
title: chapter ? chapter.title || '' : '',
|
|
1678
1711
|
durationDays: chapter && chapter.outline ? chapter.outline.durationDays : (detail.timeline.unitDays || 30),
|
|
@@ -1730,6 +1763,10 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
1730
1763
|
setSavedAt(Date.now());
|
|
1731
1764
|
setDirty(false);
|
|
1732
1765
|
if (silent !== true) notify(t('章节大纲已保存', "Outline saved"), 'success');
|
|
1766
|
+
// A length change moved the chapters after it; snapshots did not follow.
|
|
1767
|
+
if (result.shiftDays && Array.isArray(result.shifted) && result.shifted.length > 0) {
|
|
1768
|
+
if (typeof onShiftNotice === 'function') onShiftNotice({ shiftDays: result.shiftDays, shifted: result.shifted });
|
|
1769
|
+
}
|
|
1733
1770
|
await onSaved(result.chapter.chapterId);
|
|
1734
1771
|
return true;
|
|
1735
1772
|
} catch (error) {
|
|
@@ -1738,7 +1775,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
1738
1775
|
} finally {
|
|
1739
1776
|
setSaving(false);
|
|
1740
1777
|
}
|
|
1741
|
-
}, [complete, detail.projectId, chapter, form, onSaved, notify]);
|
|
1778
|
+
}, [complete, detail.projectId, chapter, form, onSaved, notify, onShiftNotice]);
|
|
1742
1779
|
|
|
1743
1780
|
// The workspace asks this page before any navigation discards its edits.
|
|
1744
1781
|
const guardRef = useRef(null);
|
|
@@ -2212,6 +2249,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
2212
2249
|
const [styleApplyJob, setStyleApplyJob] = useJob();
|
|
2213
2250
|
const [styleResult, setStyleResult] = useState(null);
|
|
2214
2251
|
const [styleError, setStyleError] = useState('');
|
|
2252
|
+
const [shiftNotice, setShiftNotice] = useState(null);
|
|
2215
2253
|
// The synopsis rewrite lives here, not in OutlinePage: the page renders
|
|
2216
2254
|
// inside the translated track, where an absolutely positioned dialog would
|
|
2217
2255
|
// be painted off-screen.
|
|
@@ -2343,7 +2381,13 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
2343
2381
|
}),
|
|
2344
2382
|
'success',
|
|
2345
2383
|
);
|
|
2346
|
-
|
|
2384
|
+
// A duration change moved the chapters after it; snapshots did not move
|
|
2385
|
+
// with them, so this is announced in a dialog instead of a toast.
|
|
2386
|
+
if (result.shiftDays && Array.isArray(result.shifted) && result.shifted.length > 0) {
|
|
2387
|
+
setShiftNotice({ shiftDays: result.shiftDays, shifted: result.shifted });
|
|
2388
|
+
} else {
|
|
2389
|
+
for (const note of notes) notify(note, 'error');
|
|
2390
|
+
}
|
|
2347
2391
|
onReload();
|
|
2348
2392
|
} else if (summarizeJob.state === 'error') {
|
|
2349
2393
|
notify(summarizeJob.error || t('同步大纲失败', 'Syncing the outline failed'), 'error');
|
|
@@ -2721,8 +2765,20 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
2721
2765
|
synopsisDraft,
|
|
2722
2766
|
onSynopsisDraftApplied: () => setSynopsisDraft(null),
|
|
2723
2767
|
revisingSynopsis,
|
|
2768
|
+
onShiftNotice: setShiftNotice,
|
|
2724
2769
|
isFirstChapter: Boolean(chapter && chapters.length > 0 && chapter.chapterId === chapters[0].chapterId),
|
|
2725
2770
|
}))),
|
|
2771
|
+
shiftNotice
|
|
2772
|
+
? h(ShiftNoticeDialog, {
|
|
2773
|
+
shiftDays: shiftNotice.shiftDays,
|
|
2774
|
+
shifted: shiftNotice.shifted,
|
|
2775
|
+
onClose: () => setShiftNotice(null),
|
|
2776
|
+
onGoTimeline: () => {
|
|
2777
|
+
setShiftNotice(null);
|
|
2778
|
+
setView('world');
|
|
2779
|
+
},
|
|
2780
|
+
})
|
|
2781
|
+
: null,
|
|
2726
2782
|
styleResult || applyingStyle
|
|
2727
2783
|
? h(StyleDialog, {
|
|
2728
2784
|
style: styleResult ? styleResult.style : '',
|
|
@@ -2832,6 +2888,9 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
2832
2888
|
const [presetTarget, setPresetTarget] = useState(null);
|
|
2833
2889
|
const [preludeTarget, setPreludeTarget] = useState(null);
|
|
2834
2890
|
const [importTarget, setImportTarget] = useState(null);
|
|
2891
|
+
// Import's outline sync can move chapters too; the notice belongs here
|
|
2892
|
+
// because the import dialog closes before the shift is known.
|
|
2893
|
+
const [shiftNotice, setShiftNotice] = useState(null);
|
|
2835
2894
|
const leaveGuard = useRef(null);
|
|
2836
2895
|
const registerLeaveGuard = useCallback((guard) => {
|
|
2837
2896
|
leaveGuard.current = guard;
|
|
@@ -2971,6 +3030,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
2971
3030
|
resolveWorkspace,
|
|
2972
3031
|
onCancel: () => setImportTarget(null),
|
|
2973
3032
|
onDone: async (result) => {
|
|
3033
|
+
const finished = importTarget;
|
|
2974
3034
|
setImportTarget(null);
|
|
2975
3035
|
await loadBoot();
|
|
2976
3036
|
const imported = (result.imported || []).length;
|
|
@@ -2986,6 +3046,26 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
2986
3046
|
}));
|
|
2987
3047
|
if (result.snapshotJobId) parts.push(t('世界快照已开始生成', 'World snapshot started'));
|
|
2988
3048
|
setToast({ text: parts.join(';'), kind: failed > 0 ? 'error' : 'success' });
|
|
3049
|
+
if (result.shiftDays && Array.isArray(result.shifted) && result.shifted.length > 0) {
|
|
3050
|
+
setShiftNotice({
|
|
3051
|
+
shiftDays: result.shiftDays,
|
|
3052
|
+
shifted: result.shifted,
|
|
3053
|
+
projectId: finished ? finished.projectId : '',
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
},
|
|
3057
|
+
})
|
|
3058
|
+
: null,
|
|
3059
|
+
shiftNotice
|
|
3060
|
+
? h(ShiftNoticeDialog, {
|
|
3061
|
+
shiftDays: shiftNotice.shiftDays,
|
|
3062
|
+
shifted: shiftNotice.shifted,
|
|
3063
|
+
scope: 'import',
|
|
3064
|
+
onClose: () => setShiftNotice(null),
|
|
3065
|
+
onGoTimeline: () => {
|
|
3066
|
+
const targetId = shiftNotice.projectId || '';
|
|
3067
|
+
setShiftNotice(null);
|
|
3068
|
+
if (targetId !== '') openProject(targetId);
|
|
2989
3069
|
},
|
|
2990
3070
|
})
|
|
2991
3071
|
: null,
|
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.
|
|
1502
|
+
revision: '2026-10-29.3',
|
|
1503
1503
|
projects,
|
|
1504
1504
|
models: {
|
|
1505
1505
|
default: modelCatalog.default || null,
|
|
@@ -2137,23 +2137,15 @@ async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note)
|
|
|
2137
2137
|
synopsisMinChars = Math.max(20, Math.min(400, synopsisMinChars));
|
|
2138
2138
|
synopsisMinChars = Math.max(1, Math.min(synopsisMinChars, synopsis.length));
|
|
2139
2139
|
|
|
2140
|
-
//
|
|
2141
|
-
|
|
2140
|
+
// The measured duration wins, and the timeline stays contiguous: every later
|
|
2141
|
+
// chapter moves with it. Snapshots are deliberately left where they are — the
|
|
2142
|
+
// Client tells the author to regenerate the ones that no longer line up.
|
|
2142
2143
|
const proposed = Number.isFinite(Number(parsed.durationDays))
|
|
2143
2144
|
? Math.max(1, Math.min(365, Math.round(Number(parsed.durationDays))))
|
|
2144
2145
|
: null;
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
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
|
-
}
|
|
2146
|
+
let durationDays = proposed === null ? chapter.durationDays : proposed;
|
|
2147
|
+
let shiftDays = 0;
|
|
2148
|
+
const shifted = [];
|
|
2157
2149
|
|
|
2158
2150
|
const saved = await mutateProject(root, projectId, (draft) => {
|
|
2159
2151
|
const target = draft.chapters.find((entry) => entry.chapterId === chapterId);
|
|
@@ -2167,13 +2159,22 @@ async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note)
|
|
|
2167
2159
|
synopsisMinChars,
|
|
2168
2160
|
savedAt: Date.now(),
|
|
2169
2161
|
};
|
|
2170
|
-
target.durationDays = durationDays;
|
|
2171
2162
|
target.updatedAt = Date.now();
|
|
2163
|
+
// Shared with the outline save and chapter import: any length change moves
|
|
2164
|
+
// the chapters that follow.
|
|
2165
|
+
const moved = applyDurationShift(draft.chapters, target, durationDays);
|
|
2166
|
+
durationDays = target.durationDays;
|
|
2167
|
+
shiftDays = moved.shiftDays;
|
|
2168
|
+
shifted.push(...moved.shifted);
|
|
2169
|
+
target.outline.durationDays = durationDays;
|
|
2172
2170
|
return { chapter: { ...target, content: undefined }, project: summarize(draft) };
|
|
2173
2171
|
});
|
|
2174
2172
|
const fresh = await readProject(root, projectId);
|
|
2175
2173
|
await exportChapterOutline(fresh, saved.chapter);
|
|
2176
2174
|
await exportProjectInfo(fresh);
|
|
2175
|
+
if (shifted.length > 0 && shiftDays !== 0) {
|
|
2176
|
+
notes.push(`本章时间长度 ${chapter.durationDays} → ${durationDays} 天,之后 ${shifted.length} 章整体顺移 ${shiftDays > 0 ? '+' : ''}${shiftDays} 天;世界快照没有跟着移动`);
|
|
2177
|
+
}
|
|
2177
2178
|
return {
|
|
2178
2179
|
synopsis,
|
|
2179
2180
|
minWords,
|
|
@@ -2182,6 +2183,8 @@ async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note)
|
|
|
2182
2183
|
synopsisMinChars,
|
|
2183
2184
|
chars,
|
|
2184
2185
|
notes,
|
|
2186
|
+
shifted,
|
|
2187
|
+
shiftDays,
|
|
2185
2188
|
chapter: saved.chapter,
|
|
2186
2189
|
};
|
|
2187
2190
|
}
|
|
@@ -2412,6 +2415,8 @@ async function startImportChapters(ctx, root, registry, body) {
|
|
|
2412
2415
|
}
|
|
2413
2416
|
|
|
2414
2417
|
const synced = [];
|
|
2418
|
+
const shifted = [];
|
|
2419
|
+
let shiftDaysTotal = 0;
|
|
2415
2420
|
if (syncOutline && imported.length > 0) {
|
|
2416
2421
|
for (let index = 0; index < imported.length; index += 1) {
|
|
2417
2422
|
const entry = imported[index];
|
|
@@ -2423,6 +2428,15 @@ async function startImportChapters(ctx, root, registry, body) {
|
|
|
2423
2428
|
job.message = note;
|
|
2424
2429
|
});
|
|
2425
2430
|
synced.push({ chapterId: entry.chapterId, synopsisChars: (result.synopsis || '').length });
|
|
2431
|
+
for (const move of (result.shifted || [])) {
|
|
2432
|
+
const seen = shifted.find((item) => item.chapterId === move.chapterId);
|
|
2433
|
+
if (seen) {
|
|
2434
|
+
seen.toDay = move.toDay;
|
|
2435
|
+
} else {
|
|
2436
|
+
shifted.push({ ...move });
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
shiftDaysTotal += Number(result.shiftDays) || 0;
|
|
2426
2440
|
} catch (error) {
|
|
2427
2441
|
failed.push({ fileName: entry.title, reason: `同步大纲失败:${error instanceof Error ? error.message : String(error)}` });
|
|
2428
2442
|
}
|
|
@@ -2443,6 +2457,8 @@ async function startImportChapters(ctx, root, registry, body) {
|
|
|
2443
2457
|
skipped,
|
|
2444
2458
|
failed,
|
|
2445
2459
|
synced,
|
|
2460
|
+
shifted,
|
|
2461
|
+
shiftDays: shiftDaysTotal,
|
|
2446
2462
|
snapshotJobId,
|
|
2447
2463
|
project: summarize(await readProject(root, projectId)),
|
|
2448
2464
|
};
|
|
@@ -2592,6 +2608,33 @@ function nextChapterIndex(project) {
|
|
|
2592
2608
|
return max + 1;
|
|
2593
2609
|
}
|
|
2594
2610
|
|
|
2611
|
+
/**
|
|
2612
|
+
* Keep story time contiguous after one chapter's length changes.
|
|
2613
|
+
*
|
|
2614
|
+
* The chapter takes the new length and every chapter starting at or after it
|
|
2615
|
+
* moves by the same number of days, so the timeline never opens a gap or an
|
|
2616
|
+
* overlap. Snapshots are not touched — the Client tells the author to update
|
|
2617
|
+
* them. Returns the moves so callers can report them.
|
|
2618
|
+
*/
|
|
2619
|
+
function applyDurationShift(chapters, chapter, nextDuration) {
|
|
2620
|
+
const previous = Math.max(1, Math.round(Number(chapter.durationDays) || 1));
|
|
2621
|
+
const next = Math.max(1, Math.round(Number(nextDuration) || previous));
|
|
2622
|
+
const shifted = [];
|
|
2623
|
+
chapter.durationDays = next;
|
|
2624
|
+
const delta = next - previous;
|
|
2625
|
+
if (delta === 0) return { shiftDays: 0, shifted };
|
|
2626
|
+
const start = Math.max(0, Math.round(Number(chapter.startDay) || 0));
|
|
2627
|
+
for (const other of chapters) {
|
|
2628
|
+
if (other.chapterId === chapter.chapterId) continue;
|
|
2629
|
+
if ((Number(other.startDay) || 0) < start) continue;
|
|
2630
|
+
const from = other.startDay;
|
|
2631
|
+
other.startDay = Math.max(start + 1, from + delta);
|
|
2632
|
+
other.updatedAt = Date.now();
|
|
2633
|
+
shifted.push({ chapterId: other.chapterId, title: other.title, fromDay: from, toDay: other.startDay });
|
|
2634
|
+
}
|
|
2635
|
+
return { shiftDays: delta, shifted };
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2595
2638
|
async function saveOutline(ctx, root, registry, body) {
|
|
2596
2639
|
const projectId = asText(body.projectId);
|
|
2597
2640
|
const chapterId = asText(body.chapterId);
|
|
@@ -2603,6 +2646,7 @@ async function saveOutline(ctx, root, registry, body) {
|
|
|
2603
2646
|
let savedProject = null;
|
|
2604
2647
|
let savedChapter = null;
|
|
2605
2648
|
let renamedFrom = null;
|
|
2649
|
+
let outlineShift = { shiftDays: 0, shifted: [] };
|
|
2606
2650
|
const result = await mutateProject(root, projectId, (project) => {
|
|
2607
2651
|
let chapter = chapterId === '' ? null : project.chapters.find((entry) => entry.chapterId === chapterId);
|
|
2608
2652
|
if (!chapter) {
|
|
@@ -2626,7 +2670,10 @@ async function saveOutline(ctx, root, registry, body) {
|
|
|
2626
2670
|
}
|
|
2627
2671
|
if (title !== '') chapter.title = title;
|
|
2628
2672
|
chapter.outline = outline;
|
|
2629
|
-
|
|
2673
|
+
// Length changes move the chapters after this one, whatever the caller is.
|
|
2674
|
+
const moved = applyDurationShift(project.chapters, chapter, outline.durationDays);
|
|
2675
|
+
outlineShift = moved;
|
|
2676
|
+
chapter.outline.durationDays = chapter.durationDays;
|
|
2630
2677
|
if (chapter.status === 'draft') chapter.status = 'outlined';
|
|
2631
2678
|
chapter.updatedAt = Date.now();
|
|
2632
2679
|
savedProject = project;
|
|
@@ -2657,7 +2704,13 @@ async function saveOutline(ctx, root, registry, body) {
|
|
|
2657
2704
|
snapshotJobId = registerSnapshot(ctx, root, registry, projectId, target).jobId;
|
|
2658
2705
|
}
|
|
2659
2706
|
}
|
|
2660
|
-
return
|
|
2707
|
+
return {
|
|
2708
|
+
...result,
|
|
2709
|
+
...(snapshotJobId === null ? {} : { snapshotJobId }),
|
|
2710
|
+
...(outlineShift.shifted.length === 0
|
|
2711
|
+
? {}
|
|
2712
|
+
: { shiftDays: outlineShift.shiftDays, shifted: outlineShift.shifted }),
|
|
2713
|
+
};
|
|
2661
2714
|
}
|
|
2662
2715
|
|
|
2663
2716
|
async function saveChapter(ctx, root, registry, body) {
|
|
@@ -3122,6 +3175,8 @@ async function startCompose(ctx, root, registry, body) {
|
|
|
3122
3175
|
const existing = existingId === ''
|
|
3123
3176
|
? null
|
|
3124
3177
|
: (project.chapters || []).find((entry) => entry.chapterId === existingId) || null;
|
|
3178
|
+
// Set inside the mutation below when re-deducing moves the chapters after it.
|
|
3179
|
+
let composeShift = { shiftDays: 0, shifted: [] };
|
|
3125
3180
|
if (existingId !== '' && !existing) throw httpError(404, `章节不存在: ${existingId}`);
|
|
3126
3181
|
|
|
3127
3182
|
// The first chapter of a work has no snapshot and no previous chapter, so
|
|
@@ -3172,9 +3227,12 @@ async function startCompose(ctx, root, registry, body) {
|
|
|
3172
3227
|
const target = current.chapters.find((entry) => entry.chapterId === draft.chapterId);
|
|
3173
3228
|
if (target) {
|
|
3174
3229
|
target.title = title;
|
|
3175
|
-
target.durationDays = durationDays;
|
|
3176
3230
|
target.outline = outline;
|
|
3177
3231
|
target.updatedAt = Date.now();
|
|
3232
|
+
// Re-deducing an existing chapter can change its length too.
|
|
3233
|
+
const moved = applyDurationShift(current.chapters, target, durationDays);
|
|
3234
|
+
composeShift = moved;
|
|
3235
|
+
target.outline.durationDays = target.durationDays;
|
|
3178
3236
|
} else {
|
|
3179
3237
|
current.chapters.push({
|
|
3180
3238
|
...draft,
|
|
@@ -3227,6 +3285,8 @@ async function startCompose(ctx, root, registry, body) {
|
|
|
3227
3285
|
synopsis,
|
|
3228
3286
|
wordCount: wordCount(content),
|
|
3229
3287
|
snapshotJobId,
|
|
3288
|
+
shiftDays: composeShift.shiftDays,
|
|
3289
|
+
shifted: composeShift.shifted,
|
|
3230
3290
|
};
|
|
3231
3291
|
});
|
|
3232
3292
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-novel-generator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.40",
|
|
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",
|