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 +1 -0
- package/client.js +74 -1
- package/index.js +31 -1
- package/package.json +1 -1
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
|
|
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.
|
|
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.
|
|
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",
|