dsh-novel-generator 0.1.25 → 0.1.26

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 +3 -1
  2. package/client.js +19 -19
  3. package/index.js +55 -1
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -66,7 +66,9 @@ dsh plugin --profile web add dsh-novel-generator
66
66
 
67
67
  ## 前置提示词(作品级)
68
68
 
69
- 作品列表每行的「前置」可以给一本书设定前置提示:内置「第一人称 / 第三人称限知 / 第三人称全知 / 自定义」四种,选中即填入文本框,文本可自由改写(最多 2000 字)。
69
+ 作品列表每行的「前置」可以给一本书设定前置提示:内置「第三人称全知(默认)/ 第三人称限知 / 第一人称 / 自定义」四种,选中即填入文本框,文本可自由改写(最多 2000 字)。
70
+
71
+ **新建作品默认就带「第三人称全知」那一段**(传统修仙文、禁贴身 POV、环境优先等 6 条),不喜欢可以直接改或清空。
70
72
 
71
73
  - 每次**生成正文、自动推演、AI 修改**时,这段文本都会以「【本书前置设定】…」的形式放在提示最前面;生成世界快照时不带(那是结构化数据任务)。
72
74
  - 保存后**下一次生成就生效**,不需要重建会话——它不依赖会话状态。
package/client.js CHANGED
@@ -480,31 +480,29 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
480
480
  }, busy ? t('保存中…', "Saving…") : t('保存', "Save")))));
481
481
  }
482
482
 
483
- /** Built-in preface prompts; the text is editable after picking one. */
484
- const PRELUDE_MODES = [
485
- { id: 'first', zh: '第一人称', en: 'First person', text: '全书使用第一人称叙述:只写「我」亲眼所见、亲耳所听、亲身所想,不描写别人的内心活动。' },
486
- { id: 'thirdLimited', zh: '第三人称限知', en: 'Third person, limited', text: '全书使用第三人称限知视角:叙述跟随主角,只呈现他能感知到的一切,不越界写其他角色的内心。' },
487
- { id: 'thirdOmniscient', zh: '第三人称全知', en: 'Third person, omniscient', text: '全书使用第三人称全知视角:叙述者可以进入任何角色的内心,但每次切换视角都要有场景依据,不要在同一场景里频繁跳视角。' },
488
- { id: 'custom', zh: '自定义', en: 'Custom', text: '' },
489
- ];
490
-
491
483
  /**
492
484
  * The work-wide preface prompt: a narrative angle preset plus anything else
493
- * the author wants pinned in front of every request.
485
+ * the author wants pinned in front of every request. The preset wording comes
486
+ * from the Host so there is one copy of the text.
494
487
  */
495
- function PreludeDialog({ project, onCancel, onSaved }) {
488
+ function PreludeDialog({ project, presets, defaultMode, onCancel, onSaved }) {
489
+ const modes = Array.isArray(presets) && presets.length ? presets : [];
490
+ const fallbackMode = defaultMode || 'thirdOmniscient';
496
491
  const current = project.prelude || null;
497
- const [mode, setMode] = useState(current && current.mode ? current.mode : '');
498
- const [text, setText] = useState(current && current.text ? current.text : '');
492
+ const initialMode = current && current.mode ? current.mode : fallbackMode;
493
+ const initialPreset = modes.find((entry) => entry.id === initialMode);
494
+ const [mode, setMode] = useState(initialMode);
495
+ const [text, setText] = useState(
496
+ current && current.text ? current.text : (initialPreset && initialPreset.text) || '',
497
+ );
499
498
  const [busy, setBusy] = useState(false);
500
499
  const [error, setError] = useState('');
501
500
 
502
501
  const pickMode = useCallback((id) => {
503
502
  setMode(id);
504
- const preset = PRELUDE_MODES.find((entry) => entry.id === id);
505
- if (preset && preset.text !== '') setText(preset.text);
506
- if (id === '') setText('');
507
- }, []);
503
+ const preset = modes.find((entry) => entry.id === id);
504
+ setText(preset && preset.text ? preset.text : '');
505
+ }, [modes]);
508
506
 
509
507
  const save = useCallback(async () => {
510
508
  setBusy(true);
@@ -534,14 +532,14 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
534
532
  onChange: (event) => pickMode(event.target.value),
535
533
  },
536
534
  h('option', { value: '' }, t('不设置', 'Not set')),
537
- PRELUDE_MODES.map((entry) => h('option', { key: entry.id, value: entry.id }, t(entry.zh, entry.en))))),
535
+ modes.map((entry) => h('option', { key: entry.id, value: entry.id }, t(entry.zh, entry.en))))),
538
536
  h('label', { className: 'dsn-field span2' },
539
537
  h('span', { className: 'dsn-label' }, t('前置提示(可自由修改,最多 2000 字)', 'Preface text (edit freely, up to 2000 characters)')),
540
538
  h('textarea', {
541
539
  className: 'dsn-textarea',
542
- style: { minHeight: '160px' },
540
+ style: { minHeight: '200px' },
543
541
  value: text,
544
- placeholder: t('每次生成正文、自动推演、AI 修改时都会放在提示最前面。例如:全书用第三人称限知;文风冷峻,少用形容词;不要出现现代网络用语。', 'Added in front of every generation, deduction and revision request. e.g. third-person limited; cold, spare prose; no modern slang.'),
542
+ placeholder: t('每次生成正文、自动推演、AI 修改时都会放在提示最前面。', 'Added in front of every generation, deduction and revision request.'),
545
543
  onChange: (event) => setText(event.target.value),
546
544
  })),
547
545
  h('div', { className: 'dsn-hint span2' }, t('已填 {n} / 2000 字;留空即表示这本书不带前置提示。', '{n} / 2000 characters; leave it empty to send no preface.', { n: String(text).length })),
@@ -2340,6 +2338,8 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
2340
2338
  preludeTarget
2341
2339
  ? h(PreludeDialog, {
2342
2340
  project: preludeTarget,
2341
+ presets: boot ? boot.preludePresets : [],
2342
+ defaultMode: boot ? boot.defaultPreludeMode : 'thirdOmniscient',
2343
2343
  onCancel: () => setPreludeTarget(null),
2344
2344
  onSaved: async (saved) => {
2345
2345
  setPreludeTarget(null);
package/index.js CHANGED
@@ -1477,7 +1477,7 @@ async function bootstrap(ctx, root) {
1477
1477
  }
1478
1478
  }
1479
1479
  return {
1480
- revision: '2026-10-27.9',
1480
+ revision: '2026-10-27.10',
1481
1481
  projects,
1482
1482
  models: {
1483
1483
  default: modelCatalog.default || null,
@@ -1494,6 +1494,9 @@ async function bootstrap(ctx, root) {
1494
1494
  failures: modelCatalog.failures || [],
1495
1495
  },
1496
1496
  presets: presetList,
1497
+ // Canonical preface wording, so the Client does not keep a second copy.
1498
+ preludePresets: PRELUDE_PRESETS,
1499
+ defaultPreludeMode: DEFAULT_PRELUDE_MODE,
1497
1500
  };
1498
1501
  }
1499
1502
 
@@ -1545,6 +1548,9 @@ async function createProject(ctx, root, body) {
1545
1548
  cwd,
1546
1549
  agentPreset: agentPreset || created.agentPreset || null,
1547
1550
  model,
1551
+ // Every new work starts with the default narrative preface; the author can
1552
+ // edit or clear it from the work list.
1553
+ prelude: defaultPrelude(),
1548
1554
  sessionId,
1549
1555
  createdAt: Date.now(),
1550
1556
  updatedAt: Date.now(),
@@ -1865,6 +1871,54 @@ async function createSessionFor(ctx, root, project) {
1865
1871
  /** Longest preface prompt accepted, in characters. */
1866
1872
  const PRELUDE_MAX_CHARS = 2000;
1867
1873
 
1874
+ /**
1875
+ * Built-in preface prompts. The Host owns the canonical wording, so the Client,
1876
+ * the creation default and any migration all read the same text.
1877
+ */
1878
+ const PRELUDE_PRESETS = [
1879
+ {
1880
+ id: 'thirdOmniscient',
1881
+ zh: '第三人称全知',
1882
+ en: 'Third person, omniscient',
1883
+ text: [
1884
+ '写作要求:传统修仙文第三人称叙事,参考忘语《凡人修仙传》风格,禁止贴身 POV 视角。',
1885
+ '',
1886
+ '1. 叙事优先用环境、场景作为句子开头,不要每一句都从主角的感受出发;',
1887
+ '2. 控制“他”字的出现频率,不要大量以“他”开头造句;',
1888
+ '3. 描写动作、环境、事件为主,不要细碎拆解主角每一丝触觉、听觉;',
1889
+ '4. 可以写主角的反应,但不要连续堆砌“他看见、他听见、他感到”;',
1890
+ '5. 句子连贯,不要大量一句一段的超短碎句;',
1891
+ '6. 以作者旁白来铺陈场景,把感官描写融入环境,不要单独拎出来写角色内心感受。',
1892
+ ].join('\n'),
1893
+ },
1894
+ {
1895
+ id: 'thirdLimited',
1896
+ zh: '第三人称限知',
1897
+ en: 'Third person, limited',
1898
+ text: '全书使用第三人称限知视角:叙述跟随主角,只呈现他能感知到的一切,不越界写其他角色的内心。',
1899
+ },
1900
+ {
1901
+ id: 'first',
1902
+ zh: '第一人称',
1903
+ en: 'First person',
1904
+ text: '全书使用第一人称叙述:只写「我」亲眼所见、亲耳所听、亲身所想,不描写别人的内心活动。',
1905
+ },
1906
+ {
1907
+ id: 'custom',
1908
+ zh: '自定义',
1909
+ en: 'Custom',
1910
+ text: '',
1911
+ },
1912
+ ];
1913
+
1914
+ /** The preface a fresh work starts with. */
1915
+ const DEFAULT_PRELUDE_MODE = 'thirdOmniscient';
1916
+
1917
+ function defaultPrelude() {
1918
+ const preset = PRELUDE_PRESETS.find((entry) => entry.id === DEFAULT_PRELUDE_MODE);
1919
+ return preset ? { mode: preset.id, text: preset.text, updatedAt: Date.now() } : null;
1920
+ }
1921
+
1868
1922
  /**
1869
1923
  * Save one work's preface prompt. `mode` only labels which preset the text came
1870
1924
  * from; the text itself is what every request carries, so the author can edit a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-novel-generator",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
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",