dsh-novel-generator 0.1.37 → 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.
- package/client.js +50 -1
- package/index.js +36 -23
- 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
|
-
|
|
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-
|
|
1502
|
+
revision: '2026-10-29.2',
|
|
1503
1503
|
projects,
|
|
1504
1504
|
models: {
|
|
1505
1505
|
default: modelCatalog.default || null,
|
|
@@ -2017,7 +2017,6 @@ async function updatePreset(ctx, root, body) {
|
|
|
2017
2017
|
* the four numbers the outline records, all measured from this one chapter.
|
|
2018
2018
|
*/
|
|
2019
2019
|
function syncOutlinePrompt(project, chapter, prose) {
|
|
2020
|
-
const span = chapterWindow(project, chapter);
|
|
2021
2020
|
const chars = wordCount(prose);
|
|
2022
2021
|
const lines = [];
|
|
2023
2022
|
lines.push('你是这部小说的编辑。作者已经写完了下面这一章,请据此同步它的章节大纲。');
|
|
@@ -2026,13 +2025,17 @@ function syncOutlinePrompt(project, chapter, prose) {
|
|
|
2026
2025
|
lines.push('');
|
|
2027
2026
|
lines.push('各项含义:');
|
|
2028
2027
|
lines.push('- synopsis:这一章的故事梗概,写清起因、经过、转折、结果,供后续章节与世界观使用;300 字以内,纯文本。');
|
|
2029
|
-
lines.push(
|
|
2028
|
+
lines.push('- durationDays:这段剧情在故事内部推进了几天,整数 1–365。');
|
|
2029
|
+
lines.push(' 只看正文里的时间线索(清晨到入夜、翌日、过了三日、雨下到第几天……)来数;');
|
|
2030
|
+
lines.push(' 正文没有时间线索时按事件密度估计:一场连续场景通常算 1 天,跨几次昼夜就往上加;');
|
|
2031
|
+
lines.push(' 大多数章节在 1–7 天之间,只有正文明确跨越更久时才写更大的数。');
|
|
2032
|
+
lines.push(' 不要参考任何已有设定或历史记录,完全按这段正文判断,不要沿用「原来记了多少天」。');
|
|
2030
2033
|
lines.push(`- minWords / maxWords:这一章应有的字数下限与上限,整数;本章正文实际 ${chars} 字,范围应当把 ${chars} 包在里面。`);
|
|
2031
2034
|
lines.push('- synopsisMinChars:上面这段梗概算「合格」的最低字数,整数 20–400,取梗概长度的八成左右。');
|
|
2032
2035
|
lines.push('');
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
lines.push(`【本章】第 ${chapter.index + 1} 章 ${chapter.title || ''}
|
|
2036
|
+
// No time span and no preface here: this is a measurement of the text, and
|
|
2037
|
+
// either would hand the model the answer it is supposed to derive.
|
|
2038
|
+
lines.push(`【本章】第 ${chapter.index + 1} 章 ${chapter.title || ''}`);
|
|
2036
2039
|
lines.push(`【正文】(共 ${chars} 字,不含空白)`);
|
|
2037
2040
|
lines.push(prose);
|
|
2038
2041
|
return lines.join('\n');
|
|
@@ -2100,7 +2103,10 @@ async function startSyncOutline(ctx, root, registry, body) {
|
|
|
2100
2103
|
* back into its outline". Used by the story page's button and by chapter import,
|
|
2101
2104
|
* so both produce identical outlines.
|
|
2102
2105
|
*/
|
|
2103
|
-
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;
|
|
2104
2110
|
const project = await readProject(root, projectId);
|
|
2105
2111
|
const chapter = project.chapters.find((entry) => entry.chapterId === chapterId);
|
|
2106
2112
|
if (!chapter) throw httpError(404, `章节不存在: ${chapterId}`);
|
|
@@ -2134,23 +2140,16 @@ async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note)
|
|
|
2134
2140
|
synopsisMinChars = Math.max(20, Math.min(400, synopsisMinChars));
|
|
2135
2141
|
synopsisMinChars = Math.max(1, Math.min(synopsisMinChars, synopsis.length));
|
|
2136
2142
|
|
|
2137
|
-
//
|
|
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.
|
|
2138
2146
|
let durationDays = chapter.durationDays;
|
|
2139
2147
|
const proposed = Number.isFinite(Number(parsed.durationDays))
|
|
2140
2148
|
? Math.max(1, Math.min(365, Math.round(Number(parsed.durationDays))))
|
|
2141
2149
|
: null;
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
.sort((a, b) => a.startDay - b.startDay)[0] || null;
|
|
2146
|
-
if (following === null) {
|
|
2147
|
-
durationDays = proposed;
|
|
2148
|
-
} else if (following.startDay === span.start + proposed) {
|
|
2149
|
-
durationDays = proposed;
|
|
2150
|
-
} else {
|
|
2151
|
-
notes.push(`下一章从第 ${following.startDay} 天开始,改时长会错位,时间长度保持 ${chapter.durationDays} 天(模型建议 ${proposed} 天)`);
|
|
2152
|
-
}
|
|
2153
|
-
}
|
|
2150
|
+
const shiftDays = proposed !== null && !keepDuration ? proposed - chapter.durationDays : 0;
|
|
2151
|
+
if (proposed !== null && !keepDuration) durationDays = proposed;
|
|
2152
|
+
const shifted = [];
|
|
2154
2153
|
|
|
2155
2154
|
const saved = await mutateProject(root, projectId, (draft) => {
|
|
2156
2155
|
const target = draft.chapters.find((entry) => entry.chapterId === chapterId);
|
|
@@ -2166,11 +2165,24 @@ async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note)
|
|
|
2166
2165
|
};
|
|
2167
2166
|
target.durationDays = durationDays;
|
|
2168
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
|
+
}
|
|
2169
2178
|
return { chapter: { ...target, content: undefined }, project: summarize(draft) };
|
|
2170
2179
|
});
|
|
2171
2180
|
const fresh = await readProject(root, projectId);
|
|
2172
2181
|
await exportChapterOutline(fresh, saved.chapter);
|
|
2173
2182
|
await exportProjectInfo(fresh);
|
|
2183
|
+
if (shifted.length > 0 && shiftDays !== 0) {
|
|
2184
|
+
notes.push(`本章时间长度 ${chapter.durationDays} → ${durationDays} 天,之后 ${shifted.length} 章整体顺移 ${shiftDays > 0 ? '+' : ''}${shiftDays} 天;世界快照没有跟着移动`);
|
|
2185
|
+
}
|
|
2174
2186
|
return {
|
|
2175
2187
|
synopsis,
|
|
2176
2188
|
minWords,
|
|
@@ -2179,6 +2191,8 @@ async function syncChapterOutline(ctx, root, projectId, chapterId, prose, note)
|
|
|
2179
2191
|
synopsisMinChars,
|
|
2180
2192
|
chars,
|
|
2181
2193
|
notes,
|
|
2194
|
+
shifted,
|
|
2195
|
+
shiftDays,
|
|
2182
2196
|
chapter: saved.chapter,
|
|
2183
2197
|
};
|
|
2184
2198
|
}
|
|
@@ -2418,7 +2432,7 @@ async function startImportChapters(ctx, root, registry, body) {
|
|
|
2418
2432
|
const chapter = project.chapters.find((item) => item.chapterId === entry.chapterId);
|
|
2419
2433
|
const result = await syncChapterOutline(ctx, root, projectId, entry.chapterId, chapter ? (chapter.content || '') : '', (note) => {
|
|
2420
2434
|
job.message = note;
|
|
2421
|
-
});
|
|
2435
|
+
}, { keepDuration: true });
|
|
2422
2436
|
synced.push({ chapterId: entry.chapterId, synopsisChars: (result.synopsis || '').length });
|
|
2423
2437
|
} catch (error) {
|
|
2424
2438
|
failed.push({ fileName: entry.title, reason: `同步大纲失败:${error instanceof Error ? error.message : String(error)}` });
|
|
@@ -2454,7 +2468,6 @@ const STYLE_MIN_CHARS = 300;
|
|
|
2454
2468
|
const STYLE_MAX_CHARS = 600;
|
|
2455
2469
|
|
|
2456
2470
|
function styleExtractPrompt(project, chapter, prose) {
|
|
2457
|
-
const span = chapterWindow(project, chapter);
|
|
2458
2471
|
const lines = [];
|
|
2459
2472
|
lines.push('你是这部小说的文字编辑。请从下面这段正文里提取这篇小说的行文风格,写成可以直接当作「写作约束」使用的条目。');
|
|
2460
2473
|
lines.push('');
|
|
@@ -2471,7 +2484,7 @@ function styleExtractPrompt(project, chapter, prose) {
|
|
|
2471
2484
|
lines.push('- 不要出现「这段文字」「作者」之类的话,也不要评价好坏;');
|
|
2472
2485
|
lines.push(`- 全文不超过 ${STYLE_MAX_CHARS} 字,宁可少写几条也要写得可执行。`);
|
|
2473
2486
|
lines.push('');
|
|
2474
|
-
lines.push(`【本章】第 ${chapter.index + 1} 章 ${chapter.title || ''}
|
|
2487
|
+
lines.push(`【本章】第 ${chapter.index + 1} 章 ${chapter.title || ''}`);
|
|
2475
2488
|
lines.push('【正文】');
|
|
2476
2489
|
lines.push(prose);
|
|
2477
2490
|
return lines.join('\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-novel-generator",
|
|
3
|
-
"version": "0.1.
|
|
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",
|