@waterwx/dsh-novel-forge 1.1.0 → 1.1.2

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/lib/index.js CHANGED
@@ -913,7 +913,12 @@ function parseJson(text, wantArray) {
913
913
  return out;
914
914
  };
915
915
  for (const candidate of candidates) for (const attempt of [candidate, repair(candidate)]) try {
916
- return JSON.parse(attempt);
916
+ const value = JSON.parse(attempt);
917
+ if (!wantArray || Array.isArray(value)) return value;
918
+ if (typeof value === "object" && value !== null) for (const key of Object.keys(value)) {
919
+ const inner = value[key];
920
+ if (Array.isArray(inner)) return inner;
921
+ }
917
922
  } catch {}
918
923
  const preview = text.length > 300 ? text.slice(0, 300) + "…" : text;
919
924
  throw new Error(`模型输出中未找到 JSON 数据。模型原始输出:${preview}`);
@@ -1092,33 +1097,59 @@ function writeSystemPrompt(project) {
1092
1097
  /**
1093
1098
  * Plan chapters from an outline (optionally for one volume).
1094
1099
  */
1095
- async function planChapters(ctx, config, project, chapterCount, volumeNo) {
1100
+ async function planChapters(ctx, config, project, chapterCount, volumeNo, outputDir) {
1096
1101
  const volume = project.volumes?.find((v) => v.no === volumeNo);
1102
+ const existing = project.chapters;
1103
+ const startNo = existing.length === 0 ? 1 : Math.max(...existing.map((c) => c.no)) + 1;
1104
+ const continuation = existing.length > 0;
1105
+ const latestFacts = continuation && Array.isArray(project.facts) ? project.facts.slice(-15).map((f) => `[第${f.chapterNo}章] ${f.text.slice(0, 150)}`).join("\n") : "";
1106
+ let prevTail = "";
1107
+ if (continuation) {
1108
+ const written = existing.filter((c) => c.status !== "pending");
1109
+ const last = written[written.length - 1];
1110
+ if (last !== void 0 && last.file !== void 0 && outputDir !== void 0) try {
1111
+ prevTail = readFileSync(join(outputDir, last.file), "utf8").replace(/^#.*$/m, "").trim().slice(-600);
1112
+ } catch {}
1113
+ }
1114
+ const outlineBlock = continuation ? (() => {
1115
+ const cut = project.outline.indexOf("七、关键剧情桥段");
1116
+ if (cut > 1500) return project.outline.slice(0, cut).trimEnd() + "\n(大纲后续剧情桥段与分卷细节从略;续写请以「上一章结尾原文」与「最新剧情状态」为剧情起点)";
1117
+ return project.outline.slice(0, 3e3) + "\n…(大纲过长已节选)";
1118
+ })() : project.outline;
1097
1119
  const user = [
1098
1120
  "请为下面这部小说规划章节。",
1099
- volume !== void 0 ? `本次只规划第 ${volume.no} 卷《${volume.title}》的章节:\n${volume.summary}` : "请规划全书开篇章节。",
1100
- `大纲如下:\n${project.outline}`,
1121
+ volume !== void 0 ? `本次只规划第 ${volume.no} 卷《${volume.title}》的章节:\n${volume.summary}` : continuation ? `本书已有 ${existing.length} 章已规划/已写作(见下方「已有章节」)。请规划**后续**章节:从第 ${startNo} 章开始。` : "请规划全书开篇章节。",
1122
+ continuation ? "【续写硬性要求】已有章节的剧情不得重写或重复,章节标题也不得与已有章节重复。以下情节均已在已有章节中发生过,后续章节**绝对不得再次出现**:穿越、暴雨送餐、滴血认主/古玉认主、首次进入墟境、用废铁淬炼首件法器、绝境肉身入鼎洗炼(该机缘已用尽)、杀死白袍弟子与灰衣随从、藏尸水沟。" : "",
1123
+ prevTail !== "" ? `【上一章(第 ${startNo - 1} 章)结尾原文】第 ${startNo} 章必须紧接此状态继续,从新的事件写起,不得回顾重述:\n${prevTail}` : "",
1124
+ latestFacts !== "" ? `【最新剧情状态(本书编年录,第 ${startNo - 1} 章结尾的事实)】规划续写时必须以此为起点,时间线、人物状态与地点衔接一致:\n${latestFacts}` : "",
1125
+ continuation ? "已有章节:\n" + existing.map((c) => {
1126
+ const sm = c.summary !== void 0 && c.summary !== "" ? `(${c.summary.slice(0, 120)})` : "";
1127
+ return `第${c.no}章《${c.title}》${sm}`;
1128
+ }).join("\n") : "",
1129
+ `全书大纲(设定参考,续写剧情不得与设定冲突):\n${outlineBlock}`,
1101
1130
  "",
1102
1131
  `请规划 ${chapterCount} 章。输出 JSON 数组(不要输出其他文字):`
1103
1132
  ].join("\n");
1104
1133
  const parsed = parseJsonArray(await complete(ctx, config, {
1105
- system: planSystemPrompt(project.volumes),
1134
+ system: planSystemPrompt(project.volumes) + (continuation ? "\n重要:本次是**续写规划**——已有章节的剧情不得重写或重复,新章节标题不得与已有章节标题相同,新章节的剧情必须从上一章结尾自然接续(人物状态、时间线、地点衔接一致)。" : ""),
1106
1135
  user,
1107
1136
  temperature: .7,
1108
- maxTokens: Math.max(config.maxTokens, 2e4)
1137
+ maxTokens: Math.max(config.maxTokens, 4e4)
1109
1138
  }));
1110
1139
  const chapters = [];
1111
- const existing = new Set(project.chapters.map((c) => c.no));
1112
- const startNo = project.chapters.length + 1;
1113
- for (let i = 0; i < Math.min(parsed.length, chapterCount); i++) {
1114
- const item = parsed[i];
1140
+ const existingNos = new Set(existing.map((c) => c.no));
1141
+ const existingTitles = new Set(existing.map((c) => c.title));
1142
+ let cursor = startNo;
1143
+ for (const item of parsed) {
1144
+ if (chapters.length >= chapterCount) break;
1115
1145
  if (typeof item !== "object" || item === null) continue;
1116
1146
  const entry = item;
1117
1147
  const title = typeof entry.title === "string" ? entry.title.trim().slice(0, 30) : "";
1118
1148
  const beats = typeof entry.beats === "string" ? entry.beats.trim() : "";
1119
1149
  if (title === "" && beats === "") continue;
1120
- const no = startNo + i;
1121
- if (existing.has(no)) continue;
1150
+ if (title !== "" && existingTitles.has(title)) continue;
1151
+ while (existingNos.has(cursor)) cursor++;
1152
+ const no = cursor++;
1122
1153
  chapters.push({
1123
1154
  no,
1124
1155
  volume: volumeOf(no, project.volumes),
@@ -3026,7 +3057,7 @@ function makeRoutes(deps) {
3026
3057
  }
3027
3058
  try {
3028
3059
  const next = project ?? createProject(outline);
3029
- const chapters = await planChapters(ctx, config, next, count, body?.volume);
3060
+ const chapters = await planChapters(ctx, config, next, count, body?.volume, config.outputDir);
3030
3061
  next.chapters.push(...chapters);
3031
3062
  next.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3032
3063
  saveProject(config.outputDir, next);