dsh-novel-generator 0.1.39 → 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 +35 -4
- package/index.js +72 -23
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -722,7 +722,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
722
722
|
* After a duration change moves the chapters that follow, the snapshots stay
|
|
723
723
|
* where they were — this dialog is the only place the author learns about it.
|
|
724
724
|
*/
|
|
725
|
-
function ShiftNoticeDialog({ shiftDays, shifted, onClose, onGoTimeline }) {
|
|
725
|
+
function ShiftNoticeDialog({ shiftDays, shifted, scope, onClose, onGoTimeline }) {
|
|
726
726
|
const list = Array.isArray(shifted) ? shifted : [];
|
|
727
727
|
const sample = list.slice(0, 6);
|
|
728
728
|
return h('div', { className: 'dsn-dialog-backdrop' },
|
|
@@ -731,7 +731,9 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
731
731
|
h('div', { className: 'dsn-title' }, t('时间线已顺移,快照需要你手动更新', 'The timeline moved — snapshots need updating')),
|
|
732
732
|
h('div', { className: 'dsn-spacer' })),
|
|
733
733
|
h('div', { className: 'dsn-dialog-body' },
|
|
734
|
-
h('div', { className: 'dsn-text span4' },
|
|
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 })),
|
|
735
737
|
list.length > 0
|
|
736
738
|
? h('div', { className: 'dsn-importlist span4' },
|
|
737
739
|
sample.map((entry) => h('div', { key: entry.chapterId, className: 'dsn-importrow' },
|
|
@@ -1703,7 +1705,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
1703
1705
|
}, busy ? t('改写中…', 'Revising…') : t('确认修改', 'Revise')))));
|
|
1704
1706
|
}
|
|
1705
1707
|
|
|
1706
|
-
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 }) {
|
|
1707
1709
|
const buildForm = useCallback(() => ({
|
|
1708
1710
|
title: chapter ? chapter.title || '' : '',
|
|
1709
1711
|
durationDays: chapter && chapter.outline ? chapter.outline.durationDays : (detail.timeline.unitDays || 30),
|
|
@@ -1761,6 +1763,10 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
1761
1763
|
setSavedAt(Date.now());
|
|
1762
1764
|
setDirty(false);
|
|
1763
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
|
+
}
|
|
1764
1770
|
await onSaved(result.chapter.chapterId);
|
|
1765
1771
|
return true;
|
|
1766
1772
|
} catch (error) {
|
|
@@ -1769,7 +1775,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
1769
1775
|
} finally {
|
|
1770
1776
|
setSaving(false);
|
|
1771
1777
|
}
|
|
1772
|
-
}, [complete, detail.projectId, chapter, form, onSaved, notify]);
|
|
1778
|
+
}, [complete, detail.projectId, chapter, form, onSaved, notify, onShiftNotice]);
|
|
1773
1779
|
|
|
1774
1780
|
// The workspace asks this page before any navigation discards its edits.
|
|
1775
1781
|
const guardRef = useRef(null);
|
|
@@ -2759,6 +2765,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
2759
2765
|
synopsisDraft,
|
|
2760
2766
|
onSynopsisDraftApplied: () => setSynopsisDraft(null),
|
|
2761
2767
|
revisingSynopsis,
|
|
2768
|
+
onShiftNotice: setShiftNotice,
|
|
2762
2769
|
isFirstChapter: Boolean(chapter && chapters.length > 0 && chapter.chapterId === chapters[0].chapterId),
|
|
2763
2770
|
}))),
|
|
2764
2771
|
shiftNotice
|
|
@@ -2881,6 +2888,9 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
2881
2888
|
const [presetTarget, setPresetTarget] = useState(null);
|
|
2882
2889
|
const [preludeTarget, setPreludeTarget] = useState(null);
|
|
2883
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);
|
|
2884
2894
|
const leaveGuard = useRef(null);
|
|
2885
2895
|
const registerLeaveGuard = useCallback((guard) => {
|
|
2886
2896
|
leaveGuard.current = guard;
|
|
@@ -3020,6 +3030,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
3020
3030
|
resolveWorkspace,
|
|
3021
3031
|
onCancel: () => setImportTarget(null),
|
|
3022
3032
|
onDone: async (result) => {
|
|
3033
|
+
const finished = importTarget;
|
|
3023
3034
|
setImportTarget(null);
|
|
3024
3035
|
await loadBoot();
|
|
3025
3036
|
const imported = (result.imported || []).length;
|
|
@@ -3035,6 +3046,26 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
3035
3046
|
}));
|
|
3036
3047
|
if (result.snapshotJobId) parts.push(t('世界快照已开始生成', 'World snapshot started'));
|
|
3037
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);
|
|
3038
3069
|
},
|
|
3039
3070
|
})
|
|
3040
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,
|
|
@@ -2103,10 +2103,7 @@ 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
|
|
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;
|
|
2106
|
+
async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note) {
|
|
2110
2107
|
const project = await readProject(root, projectId);
|
|
2111
2108
|
const chapter = project.chapters.find((entry) => entry.chapterId === chapterId);
|
|
2112
2109
|
if (!chapter) throw httpError(404, `章节不存在: ${chapterId}`);
|
|
@@ -2143,12 +2140,11 @@ async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note,
|
|
|
2143
2140
|
// The measured duration wins, and the timeline stays contiguous: every later
|
|
2144
2141
|
// chapter moves with it. Snapshots are deliberately left where they are — the
|
|
2145
2142
|
// Client tells the author to regenerate the ones that no longer line up.
|
|
2146
|
-
let durationDays = chapter.durationDays;
|
|
2147
2143
|
const proposed = Number.isFinite(Number(parsed.durationDays))
|
|
2148
2144
|
? Math.max(1, Math.min(365, Math.round(Number(parsed.durationDays))))
|
|
2149
2145
|
: null;
|
|
2150
|
-
|
|
2151
|
-
|
|
2146
|
+
let durationDays = proposed === null ? chapter.durationDays : proposed;
|
|
2147
|
+
let shiftDays = 0;
|
|
2152
2148
|
const shifted = [];
|
|
2153
2149
|
|
|
2154
2150
|
const saved = await mutateProject(root, projectId, (draft) => {
|
|
@@ -2163,18 +2159,14 @@ async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note,
|
|
|
2163
2159
|
synopsisMinChars,
|
|
2164
2160
|
savedAt: Date.now(),
|
|
2165
2161
|
};
|
|
2166
|
-
target.durationDays = durationDays;
|
|
2167
2162
|
target.updatedAt = Date.now();
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
shifted.push({ chapterId: other.chapterId, title: other.title, fromDay: from, toDay: other.startDay });
|
|
2176
|
-
}
|
|
2177
|
-
}
|
|
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;
|
|
2178
2170
|
return { chapter: { ...target, content: undefined }, project: summarize(draft) };
|
|
2179
2171
|
});
|
|
2180
2172
|
const fresh = await readProject(root, projectId);
|
|
@@ -2423,6 +2415,8 @@ async function startImportChapters(ctx, root, registry, body) {
|
|
|
2423
2415
|
}
|
|
2424
2416
|
|
|
2425
2417
|
const synced = [];
|
|
2418
|
+
const shifted = [];
|
|
2419
|
+
let shiftDaysTotal = 0;
|
|
2426
2420
|
if (syncOutline && imported.length > 0) {
|
|
2427
2421
|
for (let index = 0; index < imported.length; index += 1) {
|
|
2428
2422
|
const entry = imported[index];
|
|
@@ -2432,8 +2426,17 @@ async function startImportChapters(ctx, root, registry, body) {
|
|
|
2432
2426
|
const chapter = project.chapters.find((item) => item.chapterId === entry.chapterId);
|
|
2433
2427
|
const result = await syncChapterOutline(ctx, root, projectId, entry.chapterId, chapter ? (chapter.content || '') : '', (note) => {
|
|
2434
2428
|
job.message = note;
|
|
2435
|
-
}
|
|
2429
|
+
});
|
|
2436
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;
|
|
2437
2440
|
} catch (error) {
|
|
2438
2441
|
failed.push({ fileName: entry.title, reason: `同步大纲失败:${error instanceof Error ? error.message : String(error)}` });
|
|
2439
2442
|
}
|
|
@@ -2454,6 +2457,8 @@ async function startImportChapters(ctx, root, registry, body) {
|
|
|
2454
2457
|
skipped,
|
|
2455
2458
|
failed,
|
|
2456
2459
|
synced,
|
|
2460
|
+
shifted,
|
|
2461
|
+
shiftDays: shiftDaysTotal,
|
|
2457
2462
|
snapshotJobId,
|
|
2458
2463
|
project: summarize(await readProject(root, projectId)),
|
|
2459
2464
|
};
|
|
@@ -2603,6 +2608,33 @@ function nextChapterIndex(project) {
|
|
|
2603
2608
|
return max + 1;
|
|
2604
2609
|
}
|
|
2605
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
|
+
|
|
2606
2638
|
async function saveOutline(ctx, root, registry, body) {
|
|
2607
2639
|
const projectId = asText(body.projectId);
|
|
2608
2640
|
const chapterId = asText(body.chapterId);
|
|
@@ -2614,6 +2646,7 @@ async function saveOutline(ctx, root, registry, body) {
|
|
|
2614
2646
|
let savedProject = null;
|
|
2615
2647
|
let savedChapter = null;
|
|
2616
2648
|
let renamedFrom = null;
|
|
2649
|
+
let outlineShift = { shiftDays: 0, shifted: [] };
|
|
2617
2650
|
const result = await mutateProject(root, projectId, (project) => {
|
|
2618
2651
|
let chapter = chapterId === '' ? null : project.chapters.find((entry) => entry.chapterId === chapterId);
|
|
2619
2652
|
if (!chapter) {
|
|
@@ -2637,7 +2670,10 @@ async function saveOutline(ctx, root, registry, body) {
|
|
|
2637
2670
|
}
|
|
2638
2671
|
if (title !== '') chapter.title = title;
|
|
2639
2672
|
chapter.outline = outline;
|
|
2640
|
-
|
|
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;
|
|
2641
2677
|
if (chapter.status === 'draft') chapter.status = 'outlined';
|
|
2642
2678
|
chapter.updatedAt = Date.now();
|
|
2643
2679
|
savedProject = project;
|
|
@@ -2668,7 +2704,13 @@ async function saveOutline(ctx, root, registry, body) {
|
|
|
2668
2704
|
snapshotJobId = registerSnapshot(ctx, root, registry, projectId, target).jobId;
|
|
2669
2705
|
}
|
|
2670
2706
|
}
|
|
2671
|
-
return
|
|
2707
|
+
return {
|
|
2708
|
+
...result,
|
|
2709
|
+
...(snapshotJobId === null ? {} : { snapshotJobId }),
|
|
2710
|
+
...(outlineShift.shifted.length === 0
|
|
2711
|
+
? {}
|
|
2712
|
+
: { shiftDays: outlineShift.shiftDays, shifted: outlineShift.shifted }),
|
|
2713
|
+
};
|
|
2672
2714
|
}
|
|
2673
2715
|
|
|
2674
2716
|
async function saveChapter(ctx, root, registry, body) {
|
|
@@ -3133,6 +3175,8 @@ async function startCompose(ctx, root, registry, body) {
|
|
|
3133
3175
|
const existing = existingId === ''
|
|
3134
3176
|
? null
|
|
3135
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: [] };
|
|
3136
3180
|
if (existingId !== '' && !existing) throw httpError(404, `章节不存在: ${existingId}`);
|
|
3137
3181
|
|
|
3138
3182
|
// The first chapter of a work has no snapshot and no previous chapter, so
|
|
@@ -3183,9 +3227,12 @@ async function startCompose(ctx, root, registry, body) {
|
|
|
3183
3227
|
const target = current.chapters.find((entry) => entry.chapterId === draft.chapterId);
|
|
3184
3228
|
if (target) {
|
|
3185
3229
|
target.title = title;
|
|
3186
|
-
target.durationDays = durationDays;
|
|
3187
3230
|
target.outline = outline;
|
|
3188
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;
|
|
3189
3236
|
} else {
|
|
3190
3237
|
current.chapters.push({
|
|
3191
3238
|
...draft,
|
|
@@ -3238,6 +3285,8 @@ async function startCompose(ctx, root, registry, body) {
|
|
|
3238
3285
|
synopsis,
|
|
3239
3286
|
wordCount: wordCount(content),
|
|
3240
3287
|
snapshotJobId,
|
|
3288
|
+
shiftDays: composeShift.shiftDays,
|
|
3289
|
+
shifted: composeShift.shifted,
|
|
3241
3290
|
};
|
|
3242
3291
|
});
|
|
3243
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",
|