dsh-novel-generator 0.1.20 → 0.1.23

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 CHANGED
@@ -66,6 +66,7 @@ dsh plugin --profile web add dsh-novel-generator
66
66
 
67
67
  ## 迁移、导入与会话
68
68
 
69
+ - **改预设**:作品列表每行的「预设」改的是「下次建会话时用哪个预设」。会话在创建时就固定了预设,所以保存后会提示「重建会话后生效」——要立刻换,点同一行的「重建会话」。
69
70
  - **改目录**:作品列表每行的「改目录」会把整个作品文件夹搬到新目录,作品的 `cwd` 随之更新;目标目录已有同名非空文件夹时拒绝。
70
71
  - **导入已有作品**:列表上方的「导入已有作品…」选中一个作品文件夹(含 `作品信息.md`),插件从文件重建条目——章节、大纲、梗概、字数、时间长度、世界快照都会读回来,并新建一个会话。这既是手动迁移入口,也是 `.dsh/novel-studio` 数据丢失后的恢复路径。
71
72
  - **会话**:作品与会话一对一。会话被删除时插件会自动重建;被**归档**时探测不到(DSH 无查询接口),用「重建会话」按钮手动换一个。会话的模型会在每次任务前与作品记录的模型对齐。
package/client.js CHANGED
@@ -432,7 +432,55 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
432
432
  }, busy ? t('保存中…', "Saving…") : t('保存', "Save")))));
433
433
  }
434
434
 
435
- function ProjectsPage({ boot, resolveWorkspace, onOpen, onReload, onEditModel }) {
435
+ function PresetDialog({ project, presets, onCancel, onSaved }) {
436
+ const [preset, setPreset] = useState(project.agentPreset || '');
437
+ const [busy, setBusy] = useState(false);
438
+ const [error, setError] = useState('');
439
+ const list = Array.isArray(presets) ? presets : [];
440
+
441
+ const save = useCallback(async () => {
442
+ setBusy(true);
443
+ setError('');
444
+ try {
445
+ await api('/project/preset', { projectId: project.projectId, agentPreset: preset });
446
+ await onSaved();
447
+ } catch (saveError) {
448
+ setError(messageOf(saveError));
449
+ } finally {
450
+ setBusy(false);
451
+ }
452
+ }, [project.projectId, preset, onSaved]);
453
+
454
+ return h('div', { className: 'dsn-dialog-backdrop' },
455
+ h('div', { className: 'dsn-dialog', style: { width: 'min(460px, calc(100% - 48px))' } },
456
+ h('div', { className: 'dsn-dialog-head' },
457
+ h('div', { className: 'dsn-title' }, t('修改作品预设', 'Change work preset')),
458
+ h('div', { className: 'dsn-sub' }, project.title),
459
+ h('div', { className: 'dsn-spacer' })),
460
+ h('div', { className: 'dsn-dialog-body' },
461
+ h('label', { className: 'dsn-field span2' },
462
+ h('span', { className: 'dsn-label' }, t('预设', "Preset")),
463
+ h('select', {
464
+ className: 'dsn-select',
465
+ value: preset,
466
+ onChange: (event) => setPreset(event.target.value),
467
+ },
468
+ h('option', { value: '' }, t('默认预设', "Default preset")),
469
+ list.map((entry) => h('option', { key: entry.id, value: entry.id }, entry.name)))),
470
+ h('div', { className: 'dsn-hint span2' }, t('预设只在会话创建时生效;改完后重建会话才会用新预设。', 'A preset applies when a session is created; it takes effect after the work’s session is rebuilt.')),
471
+ error ? h('div', { className: 'dsn-hint warn span2' }, error) : null),
472
+ h('div', { className: 'dsn-dialog-foot' },
473
+ h('button', { className: 'dsn-btn ghost', type: 'button', disabled: busy, onClick: onCancel }, t('取消', "Cancel")),
474
+ h('div', { className: 'dsn-spacer' }),
475
+ h('button', {
476
+ className: 'dsn-btn primary',
477
+ type: 'button',
478
+ disabled: busy,
479
+ onClick: save,
480
+ }, busy ? t('保存中…', "Saving…") : t('保存', "Save")))));
481
+ }
482
+
483
+ function ProjectsPage({ boot, resolveWorkspace, onOpen, onReload, onEditModel, onEditPreset }) {
436
484
  const [title, setTitle] = useState('');
437
485
  const [cwd, setCwd] = useState('');
438
486
  const [preset, setPreset] = useState('');
@@ -668,6 +716,15 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
668
716
  onEditModel(project);
669
717
  },
670
718
  }, t('模型', "Model")),
719
+ h('button', {
720
+ className: 'dsn-btn ghost',
721
+ type: 'button',
722
+ title: t('修改这部作品的预设(重建会话后生效)', 'Change this work’s preset (applies after its session is rebuilt)'),
723
+ onClick: (event) => {
724
+ event.stopPropagation();
725
+ onEditPreset(project);
726
+ },
727
+ }, t('预设', "Preset")),
671
728
  h('button', {
672
729
  className: 'dsn-btn ghost',
673
730
  type: 'button',
@@ -1943,6 +2000,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1943
2000
  // Set while a work runs a model job, so leaving the panel can warn first.
1944
2001
  const [busyNow, setBusyNow] = useState(null);
1945
2002
  const [modelTarget, setModelTarget] = useState(null);
2003
+ const [presetTarget, setPresetTarget] = useState(null);
1946
2004
  const leaveGuard = useRef(null);
1947
2005
  const registerLeaveGuard = useCallback((guard) => {
1948
2006
  leaveGuard.current = guard;
@@ -2034,6 +2092,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
2034
2092
  onOpen: openProject,
2035
2093
  onReload: loadBoot,
2036
2094
  onEditModel: setModelTarget,
2095
+ onEditPreset: setPresetTarget,
2037
2096
  }))
2038
2097
  : detail === null
2039
2098
  ? h('div', { className: 'dsn-empty', style: { margin: 'auto' } }, t('正在载入作品…', "Loading the work…"))
@@ -2058,6 +2117,20 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
2058
2117
  if (warning) setError(warning);
2059
2118
  },
2060
2119
  })
2120
+ : null,
2121
+ presetTarget
2122
+ ? h(PresetDialog, {
2123
+ project: presetTarget,
2124
+ presets: boot ? boot.presets : [],
2125
+ onCancel: () => setPresetTarget(null),
2126
+ onSaved: async () => {
2127
+ setPresetTarget(null);
2128
+ await loadBoot();
2129
+ // The open work shows its preset in the top bar, so refresh it too.
2130
+ if (projectId) await loadDetail(projectId);
2131
+ setToast({ text: t('预设已更新,重建会话后生效', 'Preset updated — it applies once the work’s session is rebuilt'), kind: 'success' });
2132
+ },
2133
+ })
2061
2134
  : null);
2062
2135
  }
2063
2136
 
package/index.js CHANGED
@@ -1428,7 +1428,7 @@ async function bootstrap(ctx, root) {
1428
1428
  }
1429
1429
  }
1430
1430
  return {
1431
- revision: '2026-10-27.3',
1431
+ revision: '2026-10-27.6',
1432
1432
  projects,
1433
1433
  models: {
1434
1434
  default: modelCatalog.default || null,
@@ -1813,6 +1813,34 @@ async function createSessionFor(ctx, root, project) {
1813
1813
  }
1814
1814
  }
1815
1815
 
1816
+ /**
1817
+ * Record the preset a work's session should run under.
1818
+ *
1819
+ * A session fixes its preset when it is created, so this only changes the record
1820
+ * the next session is built from — the Client says so after saving.
1821
+ */
1822
+ async function updatePreset(ctx, root, body) {
1823
+ const projectId = asText(body.projectId);
1824
+ const requested = asText(body.agentPreset);
1825
+ if (requested !== '') {
1826
+ const presets = ctx.get('agentPresets');
1827
+ if (presets && typeof presets.list === 'function') {
1828
+ const list = await presets.list().catch(() => null);
1829
+ const known = Array.isArray(list) ? list.map((entry) => entry.id) : [];
1830
+ if (known.length && !known.includes(requested)) {
1831
+ throw httpError(400, `没有这个预设:${requested}`);
1832
+ }
1833
+ }
1834
+ }
1835
+ const project = await mutateProject(root, projectId, (draft) => {
1836
+ draft.agentPreset = requested === '' ? null : requested;
1837
+ draft.updatedAt = Date.now();
1838
+ return summarize(draft);
1839
+ });
1840
+ await exportProjectInfo(await readProject(root, projectId));
1841
+ return { project, appliesAfterRebuild: true };
1842
+ }
1843
+
1816
1844
  async function updateTimeline(root, body) {
1817
1845
  const projectId = asText(body.projectId);
1818
1846
  const unitDays = Number(body.unitDays);
@@ -2486,6 +2514,8 @@ async function dispatch(ctx, root, registry, method, path, url, body) {
2486
2514
  return updateTimeline(root, body);
2487
2515
  case '/project/model':
2488
2516
  return updateModel(ctx, root, body);
2517
+ case '/project/preset':
2518
+ return updatePreset(ctx, root, body);
2489
2519
  case '/project/rebind':
2490
2520
  return rebindProject(ctx, root, body);
2491
2521
  case '/project/move':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-novel-generator",
3
- "version": "0.1.20",
3
+ "version": "0.1.23",
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",