dsh-novel-generator 0.1.32 → 0.1.33
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/README.md +11 -3
- package/client.js +91 -54
- package/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,9 +83,17 @@ dsh plugin --profile web add dsh-novel-generator
|
|
|
83
83
|
|
|
84
84
|
## 章节故事页
|
|
85
85
|
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
86
|
+
- 正文可以直接编辑、保存;工具条上还有两个按钮:
|
|
87
|
+
- **「生成故事梗概」**:确认后把当前正文交给模型,生成一段新梗概并**直接保存、覆盖原有梗概**(正文不动)。
|
|
88
|
+
- **「AI 修改」**:填入修改意见后,模型按意见重写整章,结果只放进编辑区、不自动保存。
|
|
89
|
+
- 正文为空时两个按钮都不可点,接口也会拒绝(400);修改意见为空同样拒绝。
|
|
90
|
+
|
|
91
|
+
## 章节大纲页
|
|
92
|
+
|
|
93
|
+
- **「AI 修改梗概」**:与章节故事页的「AI 修改」区分——它只改**故事梗概**,依据是「原梗概 + 你的修改意见」,不动正文和章节名。结果填回梗概框并标记未保存,确认后点「保存」。
|
|
94
|
+
- 梗概为空或意见为空时按钮不可点,接口也会 400。
|
|
95
|
+
|
|
96
|
+
> 刻意的差别:「生成故事梗概」会覆盖保存(读的是你已经写定的正文),「AI 修改梗概」只改草稿(它改的正是你用来生成正文的依据,值得先看一眼)。
|
|
89
97
|
|
|
90
98
|
## 「小说工坊 · 作者」预设(随主插件一起装)
|
|
91
99
|
|
package/client.js
CHANGED
|
@@ -1357,7 +1357,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
1357
1357
|
}, busy ? t('改写中…', 'Revising…') : t('确认修改', 'Revise')))));
|
|
1358
1358
|
}
|
|
1359
1359
|
|
|
1360
|
-
function OutlinePage({ detail, chapter, isFirstChapter, onBack, onSaved, notify, onRegisterGuard, requestConfirm, onBusyChange }) {
|
|
1360
|
+
function OutlinePage({ detail, chapter, isFirstChapter, onBack, onSaved, notify, onRegisterGuard, requestConfirm, onBusyChange, onReviseSynopsis, synopsisDraft, onSynopsisDraftApplied, revisingSynopsis }) {
|
|
1361
1361
|
const buildForm = useCallback(() => ({
|
|
1362
1362
|
title: chapter ? chapter.title || '' : '',
|
|
1363
1363
|
durationDays: chapter && chapter.outline ? chapter.outline.durationDays : (detail.timeline.unitDays || 30),
|
|
@@ -1460,34 +1460,18 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
1460
1460
|
|
|
1461
1461
|
const generating = job && job.state === 'running' && job.kind === 'generate';
|
|
1462
1462
|
const composing = job && job.state === 'running' && job.kind === 'compose';
|
|
1463
|
-
const revisingSynopsis = job && job.state === 'running' && job.kind === 'synopsis-revise';
|
|
1464
1463
|
|
|
1465
|
-
/**
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
if (
|
|
1475
|
-
|
|
1476
|
-
return;
|
|
1477
|
-
}
|
|
1478
|
-
setSynopsisReviseError('');
|
|
1479
|
-
try {
|
|
1480
|
-
const started = await api('/outline/revise', {
|
|
1481
|
-
projectId: detail.projectId,
|
|
1482
|
-
chapterId: chapter.chapterId,
|
|
1483
|
-
synopsis: form.synopsis,
|
|
1484
|
-
instruction: String(instruction).trim(),
|
|
1485
|
-
});
|
|
1486
|
-
setJob(started);
|
|
1487
|
-
} catch (error) {
|
|
1488
|
-
setSynopsisReviseError(messageOf(error));
|
|
1489
|
-
}
|
|
1490
|
-
}, [detail.projectId, chapter, form.synopsis, setJob]);
|
|
1464
|
+
/**
|
|
1465
|
+
* A rewritten synopsis arrives from the workspace (which owns the dialog,
|
|
1466
|
+
* because the workspace stage sits outside this page's transformed track
|
|
1467
|
+
* and a dialog rendered in here would be drawn off-screen).
|
|
1468
|
+
*/
|
|
1469
|
+
useEffect(() => {
|
|
1470
|
+
if (typeof synopsisDraft !== 'string' || synopsisDraft === '') return;
|
|
1471
|
+
setForm((current) => ({ ...current, synopsis: synopsisDraft }));
|
|
1472
|
+
setDirty(true);
|
|
1473
|
+
if (typeof onSynopsisDraftApplied === 'function') onSynopsisDraftApplied();
|
|
1474
|
+
}, [synopsisDraft]);
|
|
1491
1475
|
|
|
1492
1476
|
/** 自动推演: deduce synopsis, title and the whole chapter, then save. */
|
|
1493
1477
|
const deduce = useCallback(() => {
|
|
@@ -1560,17 +1544,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
1560
1544
|
notify(t('已重新推演并保存章节', "Re-deduced and saved the chapter"), 'success');
|
|
1561
1545
|
onSaved(chapter ? chapter.chapterId : null);
|
|
1562
1546
|
}
|
|
1563
|
-
if (job.kind === 'synopsis-revise' && job.state === 'done') {
|
|
1564
|
-
const result = job.result || {};
|
|
1565
|
-
if (typeof result.synopsis === 'string' && result.synopsis.trim() !== '') {
|
|
1566
|
-
setForm((current) => ({ ...current, synopsis: result.synopsis }));
|
|
1567
|
-
setDirty(true);
|
|
1568
|
-
}
|
|
1569
|
-
setSynopsisReviseOpen(false);
|
|
1570
|
-
notify(t('AI 已改写故事梗概,确认后点「保存」', 'AI rewrote the synopsis — review it, then press Save'), 'success');
|
|
1571
|
-
}
|
|
1572
1547
|
if (job.state === 'error') {
|
|
1573
|
-
if (job.kind === 'synopsis-revise') setSynopsisReviseError(job.error || t('改写失败', 'Revision failed'));
|
|
1574
1548
|
notify(job.error || t('任务失败', "Task failed"), 'error');
|
|
1575
1549
|
}
|
|
1576
1550
|
}, [job ? job.jobId : null, job ? job.state : null]);
|
|
@@ -1589,8 +1563,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
1589
1563
|
? t('还没有故事梗概可改', 'There is no synopsis to revise yet')
|
|
1590
1564
|
: t('让 AI 按修改意见改写「故事梗概」(不动正文)', 'Let AI rewrite the synopsis from your notes (the chapter text stays)'),
|
|
1591
1565
|
onClick: () => {
|
|
1592
|
-
|
|
1593
|
-
setSynopsisReviseOpen(true);
|
|
1566
|
+
if (typeof onReviseSynopsis === 'function') onReviseSynopsis(form.synopsis);
|
|
1594
1567
|
},
|
|
1595
1568
|
}, t('AI 修改梗概', 'AI revise synopsis')),
|
|
1596
1569
|
h('button', {
|
|
@@ -1685,20 +1658,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
1685
1658
|
type: 'button',
|
|
1686
1659
|
disabled: !savedAt || Boolean(generating || composing) || !chapter,
|
|
1687
1660
|
onClick: generate,
|
|
1688
|
-
}, generating ? t('生成中…', "Generating…") : t('生成章节故事', "Generate chapter")))
|
|
1689
|
-
synopsisReviseOpen
|
|
1690
|
-
? h(SynopsisReviseDialog, {
|
|
1691
|
-
chapter,
|
|
1692
|
-
synopsis: form.synopsis,
|
|
1693
|
-
busy: Boolean(revisingSynopsis),
|
|
1694
|
-
error: synopsisReviseError,
|
|
1695
|
-
onCancel: () => {
|
|
1696
|
-
if (revisingSynopsis) return;
|
|
1697
|
-
setSynopsisReviseOpen(false);
|
|
1698
|
-
},
|
|
1699
|
-
onConfirm: startSynopsisRevise,
|
|
1700
|
-
})
|
|
1701
|
-
: null);
|
|
1661
|
+
}, generating ? t('生成中…', "Generating…") : t('生成章节故事', "Generate chapter"))));
|
|
1702
1662
|
}
|
|
1703
1663
|
|
|
1704
1664
|
/* --------------------------------------------------------- new chapter */
|
|
@@ -1933,6 +1893,13 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
1933
1893
|
const [reviseError, setReviseError] = useState('');
|
|
1934
1894
|
const [reviseJob, setReviseJob] = useJob();
|
|
1935
1895
|
const [summarizeJob, setSummarizeJob] = useJob();
|
|
1896
|
+
// The synopsis rewrite lives here, not in OutlinePage: the page renders
|
|
1897
|
+
// inside the translated track, where an absolutely positioned dialog would
|
|
1898
|
+
// be painted off-screen.
|
|
1899
|
+
const [synopsisTarget, setSynopsisTarget] = useState(null);
|
|
1900
|
+
const [synopsisError, setSynopsisError] = useState('');
|
|
1901
|
+
const [synopsisDraft, setSynopsisDraft] = useState(null);
|
|
1902
|
+
const [synopsisJob, setSynopsisJob] = useJob();
|
|
1936
1903
|
const [text, setText] = useState('');
|
|
1937
1904
|
const [dirty, setDirty] = useState(false);
|
|
1938
1905
|
const [saving, setSaving] = useState(false);
|
|
@@ -2052,6 +2019,55 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
2052
2019
|
}, [summarizeJob ? summarizeJob.jobId : null, summarizeJob ? summarizeJob.state : null, summarizeJob ? summarizeJob.message : null]);
|
|
2053
2020
|
|
|
2054
2021
|
const revising = Boolean(reviseJob && reviseJob.state === 'running');
|
|
2022
|
+
|
|
2023
|
+
/** Run the synopsis rewrite for the chapter the outline page handed up. */
|
|
2024
|
+
const sendSynopsisRevise = useCallback(async (instruction) => {
|
|
2025
|
+
const target = synopsisTarget;
|
|
2026
|
+
if (!target || !target.chapter) return;
|
|
2027
|
+
if (String(target.synopsis || '').trim() === '') {
|
|
2028
|
+
setSynopsisError(t('还没有故事梗概可改,先写一段再来', 'There is no synopsis yet — write one first.'));
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
if (String(instruction || '').trim() === '') {
|
|
2032
|
+
setSynopsisError(t('请先写清修改意见', 'Write what should change first.'));
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
setSynopsisError('');
|
|
2036
|
+
try {
|
|
2037
|
+
const started = await api('/outline/revise', {
|
|
2038
|
+
projectId: detail.projectId,
|
|
2039
|
+
chapterId: target.chapter.chapterId,
|
|
2040
|
+
synopsis: target.synopsis,
|
|
2041
|
+
instruction: String(instruction).trim(),
|
|
2042
|
+
});
|
|
2043
|
+
setSynopsisJob(started);
|
|
2044
|
+
} catch (error) {
|
|
2045
|
+
setSynopsisError(messageOf(error));
|
|
2046
|
+
}
|
|
2047
|
+
}, [synopsisTarget, detail.projectId, notify, setSynopsisJob]);
|
|
2048
|
+
|
|
2049
|
+
const revisingSynopsis = Boolean(synopsisJob && synopsisJob.state === 'running');
|
|
2050
|
+
useEffect(() => {
|
|
2051
|
+
if (!synopsisJob) return undefined;
|
|
2052
|
+
if (synopsisJob.state === 'running') {
|
|
2053
|
+
setChildBusy({
|
|
2054
|
+
label: t('正在按意见改写故事梗概…', 'Rewriting the synopsis…'),
|
|
2055
|
+
message: synopsisJob.message || '',
|
|
2056
|
+
});
|
|
2057
|
+
return undefined;
|
|
2058
|
+
}
|
|
2059
|
+
setChildBusy(null);
|
|
2060
|
+
if (synopsisJob.state === 'done') {
|
|
2061
|
+
const text = (synopsisJob.result || {}).synopsis;
|
|
2062
|
+
if (typeof text === 'string' && text.trim() !== '') setSynopsisDraft(text);
|
|
2063
|
+
setSynopsisTarget(null);
|
|
2064
|
+
notify(t('AI 已改写故事梗概,确认后点「保存」', 'AI rewrote the synopsis — review it, then press Save'), 'success');
|
|
2065
|
+
} else if (synopsisJob.state === 'error') {
|
|
2066
|
+
setSynopsisError(synopsisJob.error || t('改写失败', 'Revision failed'));
|
|
2067
|
+
}
|
|
2068
|
+
return undefined;
|
|
2069
|
+
}, [synopsisJob ? synopsisJob.jobId : null, synopsisJob ? synopsisJob.state : null, synopsisJob ? synopsisJob.message : null]);
|
|
2070
|
+
|
|
2055
2071
|
useEffect(() => {
|
|
2056
2072
|
if (!reviseJob) return undefined;
|
|
2057
2073
|
if (reviseJob.state === 'running') {
|
|
@@ -2283,6 +2299,13 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
2283
2299
|
onRegisterGuard: registerOutlineGuard,
|
|
2284
2300
|
requestConfirm: setConfirmRequest,
|
|
2285
2301
|
onBusyChange: setChildBusy,
|
|
2302
|
+
onReviseSynopsis: (synopsis) => {
|
|
2303
|
+
setSynopsisError('');
|
|
2304
|
+
setSynopsisTarget({ chapter, synopsis });
|
|
2305
|
+
},
|
|
2306
|
+
synopsisDraft,
|
|
2307
|
+
onSynopsisDraftApplied: () => setSynopsisDraft(null),
|
|
2308
|
+
revisingSynopsis,
|
|
2286
2309
|
isFirstChapter: Boolean(chapter && chapters.length > 0 && chapter.chapterId === chapters[0].chapterId),
|
|
2287
2310
|
}))),
|
|
2288
2311
|
confirmRequest
|
|
@@ -2298,6 +2321,20 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
|
|
|
2298
2321
|
},
|
|
2299
2322
|
})
|
|
2300
2323
|
: null,
|
|
2324
|
+
synopsisTarget
|
|
2325
|
+
? h(SynopsisReviseDialog, {
|
|
2326
|
+
chapter: synopsisTarget.chapter,
|
|
2327
|
+
synopsis: synopsisTarget.synopsis,
|
|
2328
|
+
busy: revisingSynopsis,
|
|
2329
|
+
error: synopsisError,
|
|
2330
|
+
onCancel: () => {
|
|
2331
|
+
if (revisingSynopsis) return;
|
|
2332
|
+
setSynopsisTarget(null);
|
|
2333
|
+
setSynopsisError('');
|
|
2334
|
+
},
|
|
2335
|
+
onConfirm: sendSynopsisRevise,
|
|
2336
|
+
})
|
|
2337
|
+
: null,
|
|
2301
2338
|
reviseOpen
|
|
2302
2339
|
? h(ReviseDialog, {
|
|
2303
2340
|
chapter,
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-novel-generator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
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",
|