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.
Files changed (4) hide show
  1. package/README.md +11 -3
  2. package/client.js +91 -54
  3. package/index.js +1 -1
  4. 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
- - 正文可以直接编辑、保存;工具条上还有「AI 修改」:填入修改意见后,模型按意见重写整章。
87
- - 正文为空时按钮不可点,接口也会拒绝(400);修改意见为空同样拒绝。
88
- - **重写结果只放进编辑区、不自动保存**,状态变成「未保存」,你确认后再点「保存」——避免 AI 的改动直接覆盖原稿。
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
- /** AI 修改故事梗概: rewrite the synopsis from a note, keep it as a draft. */
1466
- const [synopsisReviseOpen, setSynopsisReviseOpen] = useState(false);
1467
- const [synopsisReviseError, setSynopsisReviseError] = useState('');
1468
- const startSynopsisRevise = useCallback(async (instruction) => {
1469
- if (!chapter) return;
1470
- if (String(form.synopsis || '').trim() === '') {
1471
- setSynopsisReviseError(t('还没有故事梗概可改,先写一段再来', 'There is no synopsis yet — write one first.'));
1472
- return;
1473
- }
1474
- if (String(instruction || '').trim() === '') {
1475
- setSynopsisReviseError(t('请先写清修改意见', 'Write what should change first.'));
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
- setSynopsisReviseError('');
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
@@ -1499,7 +1499,7 @@ async function bootstrap(ctx, root) {
1499
1499
  }
1500
1500
  }
1501
1501
  return {
1502
- revision: '2026-10-28.2',
1502
+ revision: '2026-10-28.4',
1503
1503
  projects,
1504
1504
  models: {
1505
1505
  default: modelCatalog.default || null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-novel-generator",
3
- "version": "0.1.32",
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",