dsh-novel-generator 0.1.41 → 0.1.42

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 +35 -0
  2. package/client.js +110 -1
  3. package/index.js +156 -1
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  DSH 的常驻插件:在时间线上写小说。每个作品绑定一个普通会话,章节正文与世界快照都由那个会话里自己的 Agent 生成。
4
4
 
5
+ ## 截图
6
+
7
+ 作品列表(每行「模型 / 预设 / 前置 / 设置 / 导入章节 / 重建会话 / 改目录 / 删除」):
8
+
9
+ ![作品列表](screenshots/1-list.png)
10
+
11
+ 时间长河与世界快照:
12
+
13
+ ![时间长河](screenshots/2-timeline.png)
14
+
15
+ 章节故事页(编辑、同步章节大纲、提取行文风格、AI 修改):
16
+
17
+ ![章节故事](screenshots/3-chapter.png)
18
+
19
+ 章节大纲页(梗概、字数范围、时间长度、AI 修改梗概):
20
+
21
+ ![章节大纲](screenshots/4-outline.png)
22
+
23
+ > 这四张也在 `screenshots.json` 里声明了顺序,插件市场详情页会按这个顺序展示。
24
+
5
25
  ## 安装
6
26
 
7
27
  ```sh
@@ -96,6 +116,21 @@ dsh plugin --profile web add dsh-novel-generator
96
116
  - 内容同时写进作品的 `作品信息.md`(文末「前置提示词」一节),便于外部查看与备份。
97
117
  - 留空即表示不设置。
98
118
 
119
+ ## 作品设置:前文参考程度
120
+
121
+ 作品列表每行的「设置」里,控制**自动推演**与**生成章节故事**时对前文的参考程度:
122
+
123
+ | 选项 | 提示词里带什么 |
124
+ |---|---|
125
+ | **前一章(全文)**(默认) | 上一章整章正文,用来接住语气、场景与最后一句 |
126
+ | 前一章的后一半 | 只从上一章中间那句话开始给到结尾 |
127
+ | 不参考正文 | 只靠世界快照与各章梗概推进 |
128
+
129
+ - 上一章正文超过 8000 字时只取**最后 8000 字**(衔接看结尾,不看开头)。
130
+ - 「上一章」按起始日排序取本章之前最近的一章;它没有正文(空章节/导入未写)时该块自动省略。
131
+ - 参考块出现在提示词里「已发生的章节梗概」之后、「本章梗概」之前。
132
+ - 自动推演(`/deduce`、`/compose`)与生成正文(`/generate`)共用这一设置;同步大纲、提取风格、世界快照**不带**前文正文(那些是测量/结构化任务,带前文只会干扰判断)。
133
+
99
134
  ## 章节故事页
100
135
 
101
136
  - 正文可以直接编辑、保存;工具条上还有三个按钮:
package/client.js CHANGED
@@ -268,6 +268,19 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
268
268
  .dsn-importtitle{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary)}
269
269
  .dsn-importchars{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
270
270
  .dsn-importwarn{color:var(--dsw-alias-state-warn-primary)}
271
+ .dsn-prompt-body{display:flex;gap:12px;min-height:320px;max-height:min(58vh,520px)}
272
+ .dsn-prompt-list{display:flex;flex-direction:column;gap:4px;min-width:150px;overflow:auto}
273
+ .dsn-prompt-item{text-align:left;font-size:12px;padding:7px 10px;border-radius:8px;cursor:pointer;
274
+ border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary)}
275
+ .dsn-prompt-item:hover{background:var(--dsw-alias-bg-layer-3)}
276
+ .dsn-prompt-item.active{background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);
277
+ border-color:var(--dsw-alias-border-l2)}
278
+ .dsn-prompt-pane{flex:1;min-width:0;display:flex;flex-direction:column;gap:8px}
279
+ .dsn-prompt-when{font-size:11px;color:var(--dsw-alias-label-secondary)}
280
+ .dsn-prompt-text{flex:1;margin:0;overflow:auto;white-space:pre-wrap;word-break:break-word;
281
+ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:1.65;
282
+ padding:10px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);
283
+ background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary)}
271
284
  .dsn-progress{font-size:12px;color:var(--dsw-alias-label-secondary);display:flex;align-items:center;gap:8px}
272
285
  .dsn-spin{width:12px;height:12px;border-radius:50%;border:2px solid var(--dsw-alias-border-l2);
273
286
  border-top-color:var(--dsw-alias-brand-primary);animation:dsn-spin .9s linear infinite}
@@ -857,6 +870,85 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
857
870
  }, busy ? t('保存中…', "Saving…") : t('保存', "Save")))));
858
871
  }
859
872
 
873
+ /**
874
+ * Read-only view of every prompt the plugin sends, with the work's own text
875
+ * replaced by placeholders. The Host renders the real builders, so what is
876
+ * shown here is the actual structure — it cannot drift from what is sent.
877
+ */
878
+ function PromptsDialog({ project, onCancel }) {
879
+ const [data, setData] = useState(null);
880
+ const [active, setActive] = useState('');
881
+ const [error, setError] = useState('');
882
+ const [copied, setCopied] = useState(false);
883
+
884
+ useEffect(() => {
885
+ let alive = true;
886
+ (async () => {
887
+ try {
888
+ const result = await api('/prompts/template', { projectId: project.projectId });
889
+ if (!alive) return;
890
+ setData(result);
891
+ setActive(result.modules && result.modules.length ? result.modules[0].id : '');
892
+ } catch (loadError) {
893
+ if (alive) setError(messageOf(loadError));
894
+ }
895
+ })();
896
+ return () => { alive = false; };
897
+ }, [project.projectId]);
898
+
899
+ const modules = data ? data.modules : [];
900
+ const current = modules.find((entry) => entry.id === active) || modules[0] || null;
901
+
902
+ const copy = useCallback(async () => {
903
+ if (!current) return;
904
+ try {
905
+ await navigator.clipboard.writeText(current.text);
906
+ setCopied(true);
907
+ setTimeout(() => setCopied(false), 1500);
908
+ } catch {
909
+ setCopied(false);
910
+ }
911
+ }, [current]);
912
+
913
+ return h('div', { className: 'dsn-dialog-backdrop' },
914
+ h('div', { className: 'dsn-dialog', style: { width: 'min(980px, calc(100% - 48px))' } },
915
+ h('div', { className: 'dsn-dialog-head' },
916
+ h('div', { className: 'dsn-title' }, t('提示词一览', 'Prompts')),
917
+ h('div', { className: 'dsn-sub' }, project.title),
918
+ h('div', { className: 'dsn-spacer' }),
919
+ h('button', { className: 'dsn-btn ghost', type: 'button', onClick: onCancel }, t('关闭', 'Close'))),
920
+ h('div', { className: 'dsn-prompt-body' },
921
+ h('div', { className: 'dsn-prompt-list' },
922
+ modules.map((entry) => h('button', {
923
+ key: entry.id,
924
+ type: 'button',
925
+ className: `dsn-prompt-item${entry.id === active ? ' active' : ''}`,
926
+ onClick: () => setActive(entry.id),
927
+ }, entry.title))),
928
+ h('div', { className: 'dsn-prompt-pane' },
929
+ current
930
+ ? h(React.Fragment, null,
931
+ h('div', { className: 'dsn-prompt-when' }, current.when),
932
+ h('pre', { className: 'dsn-prompt-text' }, current.text))
933
+ : null,
934
+ error ? h('div', { className: 'dsn-hint warn' }, error) : null,
935
+ data && data.settings
936
+ ? h('div', { className: 'dsn-hint' },
937
+ t('当前设置:前文参考程度 = {ref};前置提示词 = {prelude};快照间隔 = 每 {days} 天',
938
+ 'Current settings: previous-text reference = {ref}; preface = {prelude}; snapshot interval = every {days} days',
939
+ {
940
+ ref: data.settings.referenceLabel,
941
+ prelude: data.settings.preludeSet ? t('已设置', 'set') : t('未设置', 'not set'),
942
+ days: data.settings.unitDays,
943
+ }))
944
+ : null)),
945
+ h('div', { className: 'dsn-dialog-foot' },
946
+ h('div', { className: 'dsn-hint' }, t('只读:花括号 {} 里是活数据占位,实际生成时会替换成你的内容。', 'Read-only: text in {} is a placeholder for live data, replaced with your own content when generating.')),
947
+ h('div', { className: 'dsn-spacer' }),
948
+ h('button', { className: 'dsn-btn', type: 'button', disabled: !current, onClick: copy },
949
+ copied ? t('已复制', 'Copied') : t('复制全文', 'Copy')))));
950
+ }
951
+
860
952
  /**
861
953
  * The work-wide preface prompt: a narrative angle preset plus anything else
862
954
  * the author wants pinned in front of every request. The preset wording comes
@@ -932,7 +1024,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
932
1024
  }, busy ? t('保存中…', 'Saving…') : t('保存', 'Save')))));
933
1025
  }
934
1026
 
935
- function ProjectsPage({ boot, resolveWorkspace, onOpen, onReload, onEditModel, onEditPreset, onEditPrelude, onImport, onSettings }) {
1027
+ function ProjectsPage({ boot, resolveWorkspace, onOpen, onReload, onEditModel, onEditPreset, onEditPrelude, onImport, onSettings, onPrompts }) {
936
1028
  const [title, setTitle] = useState('');
937
1029
  const [cwd, setCwd] = useState('');
938
1030
  const [preset, setPreset] = useState('');
@@ -1195,6 +1287,15 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1195
1287
  onSettings(project);
1196
1288
  },
1197
1289
  }, t('设置', 'Settings')),
1290
+ h('button', {
1291
+ className: 'dsn-btn ghost',
1292
+ type: 'button',
1293
+ title: t('查看插件会发送的全部提示词(只读,变量用占位符)', 'See every prompt the plugin sends (read-only, variables as placeholders)'),
1294
+ onClick: (event) => {
1295
+ event.stopPropagation();
1296
+ onPrompts(project);
1297
+ },
1298
+ }, t('提示词', 'Prompts')),
1198
1299
  h('button', {
1199
1300
  className: 'dsn-btn ghost',
1200
1301
  type: 'button',
@@ -2956,6 +3057,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
2956
3057
  const [preludeTarget, setPreludeTarget] = useState(null);
2957
3058
  const [importTarget, setImportTarget] = useState(null);
2958
3059
  const [settingsTarget, setSettingsTarget] = useState(null);
3060
+ const [promptsTarget, setPromptsTarget] = useState(null);
2959
3061
  // Import's outline sync can move chapters too; the notice belongs here
2960
3062
  // because the import dialog closes before the shift is known.
2961
3063
  const [shiftNotice, setShiftNotice] = useState(null);
@@ -3054,6 +3156,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
3054
3156
  onEditPrelude: setPreludeTarget,
3055
3157
  onImport: setImportTarget,
3056
3158
  onSettings: setSettingsTarget,
3159
+ onPrompts: setPromptsTarget,
3057
3160
  }))
3058
3161
  : detail === null
3059
3162
  ? h('div', { className: 'dsn-empty', style: { margin: 'auto' } }, t('正在载入作品…', "Loading the work…"))
@@ -3125,6 +3228,12 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
3125
3228
  },
3126
3229
  })
3127
3230
  : null,
3231
+ promptsTarget
3232
+ ? h(PromptsDialog, {
3233
+ project: promptsTarget,
3234
+ onCancel: () => setPromptsTarget(null),
3235
+ })
3236
+ : null,
3128
3237
  settingsTarget
3129
3238
  ? h(SettingsDialog, {
3130
3239
  project: settingsTarget,
package/index.js CHANGED
@@ -1575,7 +1575,7 @@ async function bootstrap(ctx, root) {
1575
1575
  }
1576
1576
  }
1577
1577
  return {
1578
- revision: '2026-10-29.4',
1578
+ revision: '2026-10-30.3',
1579
1579
  projects,
1580
1580
  models: {
1581
1581
  default: modelCatalog.default || null,
@@ -2665,6 +2665,159 @@ async function startStyleApply(ctx, root, registry, body) {
2665
2665
  });
2666
2666
  }
2667
2667
 
2668
+ /* ------------------------------------------------------ prompt templates */
2669
+
2670
+ /**
2671
+ * A stand-in project whose *content* is placeholder text.
2672
+ *
2673
+ * The point is that the panel renders the real prompt builders instead of a
2674
+ * hand-written copy of them: the structure can never drift from what is actually
2675
+ * sent, and the author's own text is never read or echoed.
2676
+ */
2677
+ function placeholderProject(project) {
2678
+ const snapshot = {
2679
+ snapshotId: 'placeholder',
2680
+ day: 30,
2681
+ createdAt: 0,
2682
+ unitDays: 30,
2683
+ world: '{世界观:随故事推进更新的设定}',
2684
+ characters: [{ name: '{角色名}', desc: '{身份、处境、变化}' }],
2685
+ factions: [{ name: '{势力名}', desc: '{当前状态}' }],
2686
+ // Relations are stored as plain strings ("A B 师徒"), which is what the
2687
+ // snapshot prompt asks the model for; the placeholder has to match.
2688
+ relations: ['{角色A} {角色B} {关系,例如 师徒}'],
2689
+ items: [{ name: '{物品名}', desc: '{下落或用途}' }],
2690
+ events: [{ day: 20, title: '{事件标题}', desc: '{一句话经过}' }],
2691
+ summary: '{这段时间的梗概}',
2692
+ };
2693
+ const previous = {
2694
+ chapterId: 'placeholder-previous',
2695
+ index: 0,
2696
+ title: '{上一章标题}',
2697
+ startDay: 30,
2698
+ durationDays: 30,
2699
+ wordCount: 0,
2700
+ status: 'generated',
2701
+ createdAt: 0,
2702
+ updatedAt: 0,
2703
+ content: '{上一章正文:按你的「前文参考程度」决定给多少}',
2704
+ outline: {
2705
+ synopsis: '{上一章梗概}',
2706
+ minWords: 800,
2707
+ maxWords: 3000,
2708
+ durationDays: 30,
2709
+ synopsisMinChars: 100,
2710
+ savedAt: 0,
2711
+ },
2712
+ };
2713
+ const current = {
2714
+ chapterId: 'placeholder-current',
2715
+ index: 1,
2716
+ title: '{本章标题}',
2717
+ startDay: 60,
2718
+ durationDays: 30,
2719
+ wordCount: 0,
2720
+ status: 'outlined',
2721
+ createdAt: 0,
2722
+ updatedAt: 0,
2723
+ content: '',
2724
+ outline: {
2725
+ synopsis: '{本章梗概:你在大纲页填写的内容}',
2726
+ minWords: 800,
2727
+ maxWords: 3000,
2728
+ durationDays: 30,
2729
+ synopsisMinChars: 100,
2730
+ savedAt: 0,
2731
+ },
2732
+ };
2733
+ return {
2734
+ ...project,
2735
+ prelude: { mode: 'custom', text: '{本书前置提示词:你在「前置」里写的文本}', updatedAt: 0 },
2736
+ snapshots: [snapshot],
2737
+ chapters: [previous, current],
2738
+ timeline: { ...(project.timeline || {}), unitDays: 30 },
2739
+ reference: normalizeReference(project.reference),
2740
+ };
2741
+ }
2742
+
2743
+ /** Every prompt the plugin sends, rendered against placeholder content. */
2744
+ function promptTemplates(project) {
2745
+ const stub = placeholderProject(project);
2746
+ const [previous, current] = stub.chapters;
2747
+ const mode = normalizeReference(project.reference);
2748
+ const modules = [
2749
+ {
2750
+ id: 'prelude',
2751
+ title: '本书前置设定',
2752
+ when: '写正文 / 自动推演 / AI 修改正文 / AI 修改梗概 / 提取行文风格 / 整理前置 —— 都会放在最前面',
2753
+ text: preludeBlock(stub),
2754
+ },
2755
+ {
2756
+ id: 'chapter',
2757
+ title: '写章节正文',
2758
+ when: '点「生成章节故事」,或「自动推演」走到写正文那一步',
2759
+ text: chapterPrompt(stub, current, previous),
2760
+ },
2761
+ {
2762
+ id: 'deduce',
2763
+ title: '自动推演大纲',
2764
+ when: '点「自动推演」(先推梗概、标题、时长与字数范围)',
2765
+ text: deducePrompt(stub, current),
2766
+ },
2767
+ {
2768
+ id: 'snapshot',
2769
+ title: '世界快照',
2770
+ when: '点「生成世界快照」,或章节推进到网格点时自动触发',
2771
+ text: snapshotPrompt(stub, stub.snapshots[0], [previous], 60),
2772
+ },
2773
+ {
2774
+ id: 'syncOutline',
2775
+ title: '同步章节大纲',
2776
+ when: '点「同步章节大纲」,或导入章节时勾选了自动同步',
2777
+ text: syncOutlinePrompt(stub, current, '{本章正文}'),
2778
+ },
2779
+ {
2780
+ id: 'revise',
2781
+ title: 'AI 修改正文',
2782
+ when: '故事页点「AI 修改」并填入修改意见',
2783
+ text: revisePrompt(stub, current, '{本章现有正文}', '{你填的修改意见}'),
2784
+ },
2785
+ {
2786
+ id: 'synopsisRevise',
2787
+ title: 'AI 修改梗概',
2788
+ when: '大纲页点「AI 修改梗概」并填入修改意见',
2789
+ text: synopsisRevisePrompt(stub, current, '{现有故事梗概}', '{你填的修改意见}'),
2790
+ },
2791
+ {
2792
+ id: 'styleExtract',
2793
+ title: '提取行文风格',
2794
+ when: '故事页点「提取行文风格」',
2795
+ text: styleExtractPrompt(stub, current, '{本章正文:至少 300 字才会提取}'),
2796
+ },
2797
+ {
2798
+ id: 'styleMerge',
2799
+ title: '整理进前置',
2800
+ when: '风格面板里点「写入前置」',
2801
+ text: styleMergePrompt('{现有前置提示词}', '{提取出的行文风格}'),
2802
+ },
2803
+ ];
2804
+ return {
2805
+ modules,
2806
+ settings: {
2807
+ reference: mode,
2808
+ referenceLabel: { full: '前一章全文', half: '前一章的后一半', none: '不参考正文' }[mode],
2809
+ preludeSet: Boolean(project.prelude && project.prelude.text),
2810
+ unitDays: Math.max(1, Number(project.timeline && project.timeline.unitDays) || 30),
2811
+ },
2812
+ };
2813
+ }
2814
+
2815
+ async function readPromptTemplates(root, body) {
2816
+ const projectId = asText(body.projectId);
2817
+ const project = await readProject(root, projectId);
2818
+ return promptTemplates(project);
2819
+ }
2820
+
2668
2821
  async function updateTimeline(root, body) {
2669
2822
  const projectId = asText(body.projectId);
2670
2823
  const unitDays = Number(body.unitDays);
@@ -3513,6 +3666,8 @@ async function dispatch(ctx, root, registry, method, path, url, body) {
3513
3666
  return startStyleApply(ctx, root, registry, body);
3514
3667
  case '/project/settings':
3515
3668
  return updateSettings(root, body);
3669
+ case '/prompts/template':
3670
+ return readPromptTemplates(root, body);
3516
3671
  case '/import/apply':
3517
3672
  return startImportChapters(ctx, root, registry, body);
3518
3673
  case '/outline/revise':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-novel-generator",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
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",