dsh-novel-generator 0.1.27 → 0.1.29
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 +8 -2
- package/index.js +78 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,8 +31,14 @@ dsh plugin --profile web add dsh-novel-generator
|
|
|
31
31
|
|
|
32
32
|
- 时间线以「天」计(0 = 创建作品那天),你选的「快照间隔」(7 / 15 / 30 / 90 / 180 / 365 天,可手输)决定了一条**网格**:第 15、30、45… 天。
|
|
33
33
|
- **手动**:把时间线拖到网格点上(第 N 天且 N 能被间隔整除),标题栏出现「生成世界快照 · 第 N 天」;该点已有快照时按钮变成「重新生成快照 · 第 N 天」,会替换旧的那份。两种情况都先弹确认。
|
|
34
|
-
-
|
|
35
|
-
-
|
|
34
|
+
- **自动**:只要故事时间被推进到新的网格点就会触发——**新建章节(含只有大纲的空章节)、自动推演、生成正文、手动保存正文**都算。判定是幂等的:已经存在的点不会被普通保存再次生成。
|
|
35
|
+
- **每个点收录哪些章节**(`[上一份快照日, 本点)` 半开区间):
|
|
36
|
+
- 只收录**结束时间严格早于本点**的章节——**结束在本点那一天的那一章不算**,它正是这次快照要"为之而拍"的那段,快照生成时它还没写出来;
|
|
37
|
+
- 也不收录上一份快照已经收录过的章节(`结束日 < 上一快照日` 的跳过),所以**每一章恰好被一份快照收录一次**。
|
|
38
|
+
- 例:间隔 30、章节各 10 天 → 第 30 天快照收录第 1、2 章;第 60 天快照收录第 3、4、5 章。
|
|
39
|
+
- 这样改间隔不会把旧快照重排:网格随当前间隔移动,自动生成只沿着链条向前推进,断档的地方留给你手动补。
|
|
40
|
+
- **空段落点不调用模型**:如果某个网格点没有可收录的章节(例如一章就占满整个间隔),插件直接把上一版快照抄过来、注明"这段时间没有已写章节",既省一次模型调用,也避免模型凭空编出世界设定——那种编造会污染后续所有章节的提示。落点那一章不会因此丢失:写下一章时它照样出现在「已发生的章节梗概」里。
|
|
41
|
+
- 同一部作品同一时间只跑一个快照任务,避免并发把同一个点重复生成。
|
|
36
42
|
|
|
37
43
|
- **重置**:时间线右上角的「重置快照」会清空全部快照,再按当前间隔从第 1 格逐格重建到故事末端(确认框里写明要生成几份)。
|
|
38
44
|
- **物品 / 事件**:快照里除了世界观、角色、势力、关系,还有物品(名称 + 下落/用途)和事件(故事天数 + 标题 + 一句话),事件按时间从新到旧排列。
|
package/index.js
CHANGED
|
@@ -1201,7 +1201,10 @@ function stretchBefore(project, chapter, snapshot) {
|
|
|
1201
1201
|
return (project.chapters || [])
|
|
1202
1202
|
.filter((entry) => entry.chapterId !== chapter.chapterId
|
|
1203
1203
|
&& entry.startDay < chapter.startDay
|
|
1204
|
-
|
|
1204
|
+
// The snapshot at day `base` covers chapters ending strictly before it, so
|
|
1205
|
+
// a chapter ending exactly on that day is still unwritten history for this
|
|
1206
|
+
// stretch and must be handed to the writer (>= rather than >).
|
|
1207
|
+
&& chapterWindow(project, entry).end >= base
|
|
1205
1208
|
&& ((entry.outline && entry.outline.synopsis) || '').trim() !== '')
|
|
1206
1209
|
.sort((a, b) => a.startDay - b.startDay);
|
|
1207
1210
|
}
|
|
@@ -1496,7 +1499,7 @@ async function bootstrap(ctx, root) {
|
|
|
1496
1499
|
}
|
|
1497
1500
|
}
|
|
1498
1501
|
return {
|
|
1499
|
-
revision: '2026-10-27.
|
|
1502
|
+
revision: '2026-10-27.17',
|
|
1500
1503
|
projects,
|
|
1501
1504
|
models: {
|
|
1502
1505
|
default: modelCatalog.default || null,
|
|
@@ -2046,7 +2049,7 @@ function nextChapterIndex(project) {
|
|
|
2046
2049
|
return max + 1;
|
|
2047
2050
|
}
|
|
2048
2051
|
|
|
2049
|
-
async function saveOutline(root, body) {
|
|
2052
|
+
async function saveOutline(ctx, root, registry, body) {
|
|
2050
2053
|
const projectId = asText(body.projectId);
|
|
2051
2054
|
const chapterId = asText(body.chapterId);
|
|
2052
2055
|
const outline = normalizeOutline(body.outline);
|
|
@@ -2100,10 +2103,21 @@ async function saveOutline(root, body) {
|
|
|
2100
2103
|
await exportChapterOutline(savedProject, savedChapter);
|
|
2101
2104
|
await exportProjectInfo(savedProject);
|
|
2102
2105
|
}
|
|
2103
|
-
|
|
2106
|
+
// Creating (or re-timing) a chapter that brings the story onto an interval
|
|
2107
|
+
// point is what earns the snapshot for that point: the chapter ending exactly
|
|
2108
|
+
// there is still unwritten, so the snapshot is built from what came before it.
|
|
2109
|
+
let snapshotJobId = null;
|
|
2110
|
+
if (savedChapter) {
|
|
2111
|
+
const fresh = await readProject(root, projectId);
|
|
2112
|
+
const target = autoSnapshotTarget(fresh, chapterWindow(fresh, savedChapter).end);
|
|
2113
|
+
if (target !== null) {
|
|
2114
|
+
snapshotJobId = registerSnapshot(ctx, root, registry, projectId, target).jobId;
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
return snapshotJobId === null ? result : { ...result, snapshotJobId };
|
|
2104
2118
|
}
|
|
2105
2119
|
|
|
2106
|
-
async function saveChapter(root, body) {
|
|
2120
|
+
async function saveChapter(ctx, root, registry, body) {
|
|
2107
2121
|
const projectId = asText(body.projectId);
|
|
2108
2122
|
const chapterId = asText(body.chapterId);
|
|
2109
2123
|
const content = typeof body.content === 'string' ? body.content : '';
|
|
@@ -2123,7 +2137,16 @@ async function saveChapter(root, body) {
|
|
|
2123
2137
|
return { chapter: { ...chapter, content: undefined }, project: summarize(project) };
|
|
2124
2138
|
});
|
|
2125
2139
|
await exportChapter(savedProject, savedChapter, content);
|
|
2126
|
-
|
|
2140
|
+
// Saving prose is the other way story time advances, so a point that is due
|
|
2141
|
+
// gets its snapshot here too. The check is idempotent: an existing point is
|
|
2142
|
+
// never re-created by an ordinary save.
|
|
2143
|
+
let snapshotJobId = null;
|
|
2144
|
+
if (savedChapter) {
|
|
2145
|
+
const fresh = await readProject(root, projectId);
|
|
2146
|
+
const target = autoSnapshotTarget(fresh, chapterWindow(fresh, savedChapter).end);
|
|
2147
|
+
if (target !== null) snapshotJobId = registerSnapshot(ctx, root, registry, projectId, target).jobId;
|
|
2148
|
+
}
|
|
2149
|
+
return snapshotJobId === null ? result : { ...result, snapshotJobId };
|
|
2127
2150
|
}
|
|
2128
2151
|
|
|
2129
2152
|
/** Display name the plugin gives the sidebar workspace that holds its sessions. */
|
|
@@ -2256,15 +2279,21 @@ function previousChapter(project, chapter) {
|
|
|
2256
2279
|
}
|
|
2257
2280
|
|
|
2258
2281
|
/**
|
|
2259
|
-
* Chapters that
|
|
2260
|
-
*
|
|
2261
|
-
*
|
|
2282
|
+
* Chapters that feed the snapshot at `point`: the stretch `[fromDay, point)`.
|
|
2283
|
+
*
|
|
2284
|
+
* A chapter belongs to the snapshot of the *next* point after it ends:
|
|
2285
|
+
*
|
|
2286
|
+
* - `end < point` — the chapter ending exactly on the point is the one this
|
|
2287
|
+
* snapshot is being taken for; it is still unwritten when the snapshot is
|
|
2288
|
+
* created, so counting it would pull unwritten plot into the world state.
|
|
2289
|
+
* - `end >= fromDay` — every chapter is read by exactly one snapshot, and the
|
|
2290
|
+
* previous snapshot's last chapter is not read a second time.
|
|
2262
2291
|
*/
|
|
2263
2292
|
function chaptersForPoint(project, fromDay, point) {
|
|
2264
2293
|
return (project.chapters || [])
|
|
2265
2294
|
.filter((chapter) => {
|
|
2266
2295
|
const span = chapterWindow(project, chapter);
|
|
2267
|
-
if (span.end
|
|
2296
|
+
if (span.end >= point || span.end < fromDay) return false;
|
|
2268
2297
|
return ((chapter.outline && chapter.outline.synopsis) || '').trim() !== ''
|
|
2269
2298
|
|| String(chapter.content || '').trim() !== '';
|
|
2270
2299
|
})
|
|
@@ -2278,6 +2307,13 @@ function chaptersForPoint(project, fromDay, point) {
|
|
|
2278
2307
|
* in order instead of skipping to the end.
|
|
2279
2308
|
*/
|
|
2280
2309
|
function registerSnapshot(ctx, root, registry, projectId, targetDay) {
|
|
2310
|
+
// One snapshot job per work at a time: two concurrent cascades would each read
|
|
2311
|
+
// a half-written snapshot list and produce the same point twice.
|
|
2312
|
+
for (const running of registry.jobs.values()) {
|
|
2313
|
+
if (running.kind === 'snapshot' && running.projectId === projectId && running.state === 'running') {
|
|
2314
|
+
return running;
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2281
2317
|
return startJob(registry, { kind: 'snapshot', projectId }, async (job) => {
|
|
2282
2318
|
// The sequence is fixed from the state at the start of the job: recomputing
|
|
2283
2319
|
// it every round would regenerate the last point forever once it exists.
|
|
@@ -2297,6 +2333,36 @@ function registerSnapshot(ctx, root, registry, projectId, targetDay) {
|
|
|
2297
2333
|
const project = await readProject(root, projectId);
|
|
2298
2334
|
const base = (project.snapshots || []).filter((snapshot) => snapshot.day < point).pop() || null;
|
|
2299
2335
|
const chapters = chaptersForPoint(project, base ? base.day : 0, point);
|
|
2336
|
+
// A point whose stretch holds no chapter is a blank update: carry the
|
|
2337
|
+
// previous snapshot forward verbatim instead of asking the model to invent
|
|
2338
|
+
// a world out of nothing — that invented text would then feed every later
|
|
2339
|
+
// chapter. No material, no call.
|
|
2340
|
+
if (chapters.length === 0) {
|
|
2341
|
+
const carried = {
|
|
2342
|
+
snapshotId: randomUUID(),
|
|
2343
|
+
day: point,
|
|
2344
|
+
createdAt: Date.now(),
|
|
2345
|
+
unitDays: unit,
|
|
2346
|
+
world: (base && base.world) || '',
|
|
2347
|
+
characters: (base && base.characters) || [],
|
|
2348
|
+
factions: (base && base.factions) || [],
|
|
2349
|
+
relations: (base && base.relations) || [],
|
|
2350
|
+
items: (base && base.items) || [],
|
|
2351
|
+
events: (base && base.events) || [],
|
|
2352
|
+
summary: base
|
|
2353
|
+
? `第 ${base.day} 天至第 ${point} 天之间没有已写章节,沿用上一版设定。`
|
|
2354
|
+
: `第 ${point} 天之前还没有已写章节。`,
|
|
2355
|
+
};
|
|
2356
|
+
await mutateProject(root, projectId, (draft) => {
|
|
2357
|
+
draft.snapshots = (draft.snapshots || []).filter((entry) => entry.day !== point);
|
|
2358
|
+
draft.snapshots.push(carried);
|
|
2359
|
+
draft.snapshots.sort((a, b) => a.day - b.day);
|
|
2360
|
+
return carried;
|
|
2361
|
+
});
|
|
2362
|
+
await exportSnapshot(project, carried);
|
|
2363
|
+
made.push(point);
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2300
2366
|
job.message = `正在生成第 ${point} 天的世界快照(第 ${made.length + 1}/${points.length} 份,目标第 ${target} 天)`;
|
|
2301
2367
|
const reply = await askAgent(ctx, root, project, snapshotPrompt(project, base, chapters, point), (note) => {
|
|
2302
2368
|
job.message = note;
|
|
@@ -2733,9 +2799,9 @@ async function dispatch(ctx, root, registry, method, path, url, body) {
|
|
|
2733
2799
|
case '/project/import':
|
|
2734
2800
|
return importProject(ctx, root, body);
|
|
2735
2801
|
case '/outline/save':
|
|
2736
|
-
return saveOutline(root, body);
|
|
2802
|
+
return saveOutline(ctx, root, registry, body);
|
|
2737
2803
|
case '/chapter/save':
|
|
2738
|
-
return saveChapter(root, body);
|
|
2804
|
+
return saveChapter(ctx, root, registry, body);
|
|
2739
2805
|
case '/generate':
|
|
2740
2806
|
return startGenerate(ctx, root, registry, body);
|
|
2741
2807
|
case '/chapter/revise':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-novel-generator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
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",
|