dsh-novel-generator 0.1.11 → 0.1.12
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/index.js +102 -40
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -835,22 +835,59 @@ function normalizeSnapshot(raw) {
|
|
|
835
835
|
|
|
836
836
|
/* ------------------------------------------------------------ agent work */
|
|
837
837
|
|
|
838
|
+
/**
|
|
839
|
+
* Bind a fresh session to a work whose old one disappeared (deleted in the
|
|
840
|
+
* sidebar, or gone from the session store). The work's memory — snapshots,
|
|
841
|
+
* chapters, outlines — lives in files, so a new session loses only chat
|
|
842
|
+
* history, not the novel itself.
|
|
843
|
+
*/
|
|
844
|
+
async function rebindSession(ctx, root, project) {
|
|
845
|
+
const controller = ctx.get('sessionController');
|
|
846
|
+
if (controller === undefined) throw httpError(503, '当前部署缺少 sessionController 服务,无法为作品重建会话');
|
|
847
|
+
const created = await controller.create(project.agentPreset ? { cwd: project.cwd, agentPreset: project.agentPreset } : { cwd: project.cwd });
|
|
848
|
+
const sessionId = created && created.sessionId ? created.sessionId : null;
|
|
849
|
+
if (!sessionId) throw httpError(502, '没能为作品重建会话');
|
|
850
|
+
if (project.model) {
|
|
851
|
+
try {
|
|
852
|
+
await controller.selectModel({ sessionId, ...project.model });
|
|
853
|
+
} catch {
|
|
854
|
+
// The rebuilt session still works with the profile's default model.
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
project.sessionId = sessionId;
|
|
858
|
+
await mutateProject(root, project.projectId, (draft) => {
|
|
859
|
+
draft.sessionId = sessionId;
|
|
860
|
+
});
|
|
861
|
+
try {
|
|
862
|
+
await ensureWorkspace(ctx, project);
|
|
863
|
+
} catch {
|
|
864
|
+
// Grouping is cosmetic; the session works either way.
|
|
865
|
+
}
|
|
866
|
+
const result = await controller.resolveAgent(sessionId);
|
|
867
|
+
if (!result || !result.agent) throw httpError(502, '重建的会话无法启动');
|
|
868
|
+
return { controller, agent: result.agent };
|
|
869
|
+
}
|
|
870
|
+
|
|
838
871
|
/** Resolve (or resume) the ordinary Agent that owns one project's Session. */
|
|
839
|
-
async function resolveAgent(ctx, project) {
|
|
872
|
+
async function resolveAgent(ctx, root, project, onNote) {
|
|
840
873
|
const controller = ctx.get('sessionController');
|
|
841
874
|
if (controller === undefined) throw httpError(503, '当前部署缺少 sessionController 服务,无法驱动创作会话');
|
|
842
|
-
if (!project.sessionId)
|
|
843
|
-
|
|
875
|
+
if (!project.sessionId) {
|
|
876
|
+
const rebuilt = await rebindSession(ctx, root, project);
|
|
877
|
+
if (onNote) onNote('作品没有绑定会话,已新建一个会话');
|
|
878
|
+
return rebuilt;
|
|
879
|
+
}
|
|
880
|
+
let result = null;
|
|
844
881
|
try {
|
|
845
882
|
result = await controller.resolveAgent(project.sessionId);
|
|
846
|
-
} catch
|
|
847
|
-
|
|
848
|
-
}
|
|
849
|
-
if (!result || !result.agent) {
|
|
850
|
-
const code = result && result.error && result.error.code ? result.error.code : 'unknown';
|
|
851
|
-
throw httpError(502, `无法恢复作品会话(${code})`);
|
|
883
|
+
} catch {
|
|
884
|
+
result = null;
|
|
852
885
|
}
|
|
853
|
-
return { controller, agent: result.agent };
|
|
886
|
+
if (result && result.agent) return { controller, agent: result.agent };
|
|
887
|
+
// The old session is gone: rebuild instead of failing every later call.
|
|
888
|
+
const rebuilt = await rebindSession(ctx, root, project);
|
|
889
|
+
if (onNote) onNote('原作品会话已不存在,已自动新建一个会话继续');
|
|
890
|
+
return rebuilt;
|
|
854
891
|
}
|
|
855
892
|
|
|
856
893
|
function withTimeout(promise, ms, onTimeout) {
|
|
@@ -868,32 +905,47 @@ function withTimeout(promise, ms, onTimeout) {
|
|
|
868
905
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
869
906
|
}
|
|
870
907
|
|
|
871
|
-
/**
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
908
|
+
/**
|
|
909
|
+
* Send one prompt into the project's own Session and return the reply text.
|
|
910
|
+
* A freshly rebuilt session occasionally answers nothing on its first turn, so
|
|
911
|
+
* one silent reply earns a single retry before the job fails.
|
|
912
|
+
*/
|
|
913
|
+
async function askAgent(ctx, root, project, prompt, onNote) {
|
|
914
|
+
let rebuilt = false;
|
|
915
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
916
|
+
const before = project.sessionId;
|
|
917
|
+
const { controller, agent } = await resolveAgent(ctx, root, project, attempt === 0 ? onNote : undefined);
|
|
918
|
+
if (project.sessionId !== before) rebuilt = true;
|
|
919
|
+
const admission = new AbortController();
|
|
920
|
+
await withTimeout(controller.prompt({
|
|
921
|
+
sessionId: project.sessionId,
|
|
922
|
+
requestId: `novel-${randomUUID()}`,
|
|
923
|
+
mode: 'queue',
|
|
924
|
+
content: [{ type: 'text', text: prompt }],
|
|
925
|
+
clientTimeZone: 'Asia/Shanghai',
|
|
926
|
+
}, admission.signal), 60 * 1000, () => admission.abort());
|
|
927
|
+
await withTimeout(agent.whenIdle(), AGENT_TIMEOUT_MS, () => {
|
|
928
|
+
try {
|
|
929
|
+
agent.cancel({ kind: 'hook', reason: 'novel-studio generation timeout' });
|
|
930
|
+
} catch {
|
|
931
|
+
// Cancellation is best effort on the failure path.
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
const messages = agent.session.deriveMessages();
|
|
935
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
936
|
+
const message = messages[index];
|
|
937
|
+
if (!message || message.role !== 'assistant') continue;
|
|
938
|
+
const text = textOfMessage(message);
|
|
939
|
+
if (text !== '') return text;
|
|
940
|
+
}
|
|
941
|
+
if (attempt === 0) {
|
|
942
|
+
if (onNote) onNote('模型这一轮没有回复,重试一次');
|
|
943
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
887
944
|
}
|
|
888
|
-
});
|
|
889
|
-
const messages = agent.session.deriveMessages();
|
|
890
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
891
|
-
const message = messages[index];
|
|
892
|
-
if (!message || message.role !== 'assistant') continue;
|
|
893
|
-
const text = textOfMessage(message);
|
|
894
|
-
if (text !== '') return text;
|
|
895
945
|
}
|
|
896
|
-
throw
|
|
946
|
+
throw rebuilt
|
|
947
|
+
? httpError(502, '作品的旧会话已不存在,已为它新建一个会话,但模型没有回复;请在侧栏打开这部作品的会话(「小说工坊」分组里)后再试一次')
|
|
948
|
+
: httpError(502, '模型没有返回任何正文');
|
|
897
949
|
}
|
|
898
950
|
|
|
899
951
|
function snapshotBlock(snapshot) {
|
|
@@ -1187,7 +1239,7 @@ async function bootstrap(ctx, root) {
|
|
|
1187
1239
|
}
|
|
1188
1240
|
}
|
|
1189
1241
|
return {
|
|
1190
|
-
revision: '2026-10-20.
|
|
1242
|
+
revision: '2026-10-20.35',
|
|
1191
1243
|
projects,
|
|
1192
1244
|
models: {
|
|
1193
1245
|
default: modelCatalog.default || null,
|
|
@@ -1565,7 +1617,9 @@ function registerSnapshot(ctx, root, registry, projectId, targetDay) {
|
|
|
1565
1617
|
const base = (project.snapshots || []).filter((snapshot) => snapshot.day < point).pop() || null;
|
|
1566
1618
|
const chapters = chaptersForPoint(project, base ? base.day : 0, point);
|
|
1567
1619
|
job.message = `正在生成第 ${point} 天的世界快照(第 ${made.length + 1}/${points.length} 份,目标第 ${target} 天)`;
|
|
1568
|
-
const reply = await askAgent(ctx, project, snapshotPrompt(project, base, chapters, point))
|
|
1620
|
+
const reply = await askAgent(ctx, root, project, snapshotPrompt(project, base, chapters, point), (note) => {
|
|
1621
|
+
job.message = note;
|
|
1622
|
+
});
|
|
1569
1623
|
const parsed = parseJsonReply(reply);
|
|
1570
1624
|
if (!parsed) throw httpError(502, '模型没有返回可解析的快照 JSON');
|
|
1571
1625
|
const normalized = normalizeSnapshot(parsed);
|
|
@@ -1665,7 +1719,9 @@ async function startGenerate(ctx, root, registry, body) {
|
|
|
1665
1719
|
.filter((entry) => entry.chapterId !== chapter.chapterId && entry.startDay <= chapter.startDay)
|
|
1666
1720
|
.sort((a, b) => b.startDay - a.startDay)[0] || null;
|
|
1667
1721
|
job.message = '正在生成章节正文';
|
|
1668
|
-
const content = await askAgent(ctx, project, chapterPrompt(project, chapter, previous))
|
|
1722
|
+
const content = await askAgent(ctx, root, project, chapterPrompt(project, chapter, previous), (note) => {
|
|
1723
|
+
job.message = note;
|
|
1724
|
+
});
|
|
1669
1725
|
const saved = await mutateProject(root, projectId, (draft) => {
|
|
1670
1726
|
const target = draft.chapters.find((entry) => entry.chapterId === chapterId);
|
|
1671
1727
|
if (!target) throw httpError(404, `章节不存在: ${chapterId}`);
|
|
@@ -1753,7 +1809,9 @@ async function startCompose(ctx, root, registry, body) {
|
|
|
1753
1809
|
assertSnapshotFresh(project, draft);
|
|
1754
1810
|
|
|
1755
1811
|
job.message = '正在推演本章梗概与名称';
|
|
1756
|
-
const deduceReply = await askAgent(ctx, project, deducePrompt(project, draft))
|
|
1812
|
+
const deduceReply = await askAgent(ctx, root, project, deducePrompt(project, draft), (note) => {
|
|
1813
|
+
job.message = note;
|
|
1814
|
+
});
|
|
1757
1815
|
const deduced = parseJsonReply(deduceReply) || {};
|
|
1758
1816
|
const synopsis = asText(firstOf(deduced, ['synopsis', '梗概', 'summary']));
|
|
1759
1817
|
if (synopsis === '') throw httpError(502, '模型没有给出章节梗概');
|
|
@@ -1797,7 +1855,9 @@ async function startCompose(ctx, root, registry, body) {
|
|
|
1797
1855
|
const fresh = await readProject(root, projectId);
|
|
1798
1856
|
const target = fresh.chapters.find((entry) => entry.chapterId === chapterId);
|
|
1799
1857
|
if (!target) throw httpError(500, '章节写入失败');
|
|
1800
|
-
const content = await askAgent(ctx, fresh, chapterPrompt(fresh, target, previous))
|
|
1858
|
+
const content = await askAgent(ctx, root, fresh, chapterPrompt(fresh, target, previous), (note) => {
|
|
1859
|
+
job.message = note;
|
|
1860
|
+
});
|
|
1801
1861
|
|
|
1802
1862
|
await mutateProject(root, projectId, (current) => {
|
|
1803
1863
|
const entry = current.chapters.find((item) => item.chapterId === chapterId);
|
|
@@ -1845,7 +1905,9 @@ async function startDeduce(ctx, root, registry, body) {
|
|
|
1845
1905
|
const project = await readProject(root, projectId);
|
|
1846
1906
|
const chapter = chapterId === '' ? null : project.chapters.find((entry) => entry.chapterId === chapterId) || null;
|
|
1847
1907
|
job.message = '正在推演下一章大纲';
|
|
1848
|
-
const reply = await askAgent(ctx, project, deducePrompt(project, chapter))
|
|
1908
|
+
const reply = await askAgent(ctx, root, project, deducePrompt(project, chapter), (note) => {
|
|
1909
|
+
job.message = note;
|
|
1910
|
+
});
|
|
1849
1911
|
const parsed = parseJsonReply(reply);
|
|
1850
1912
|
if (!parsed) throw httpError(502, '模型没有返回可解析的大纲 JSON');
|
|
1851
1913
|
const synopsis = asText(firstOf(parsed, ['synopsis', '梗概', 'summary']));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-novel-generator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
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",
|