@waterwx/dsh-novel-forge 1.3.0-alpha → 1.3.1-alpha

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
@@ -3653,6 +3653,16 @@ function renderTensionBlock(project, chapterNo) {
3653
3653
  * @param project 项目状态。
3654
3654
  * @returns 问题清单(可能为空)。
3655
3655
  */
3656
+ /**
3657
+ * run 型问题(连续同值 / 连续高位 / 连续低位)的受影响章号。
3658
+ * 返回**完整区间**而不是首尾两点:中间章同样受这条曲线问题影响,
3659
+ * 面板要能在每一章上给出「一键按建议修订」,合并层也要能把建议落到中间章。
3660
+ */
3661
+ function runChapters(from, to) {
3662
+ const out = [];
3663
+ for (let no = from; no <= to; no++) out.push(no);
3664
+ return out;
3665
+ }
3656
3666
  function detectTensionIssues(project) {
3657
3667
  const values = [...project.chapters ?? []].sort((a, b) => a.no - b.no).map((c) => ({
3658
3668
  no: c.no,
@@ -3681,7 +3691,7 @@ function detectTensionIssues(project) {
3681
3691
  const from = values[i - run + 1].no;
3682
3692
  issues.push({
3683
3693
  severity: "medium",
3684
- chapters: [from, values[i].no],
3694
+ chapters: runChapters(from, values[i].no),
3685
3695
  item: `第${from}-${values[i].no}章张力几乎无变化(连续 ${run} 章)`,
3686
3696
  suggestion: "安排一次明显抬升或一次释放,让曲线有起伏,避免读者疲劳。"
3687
3697
  });
@@ -3695,7 +3705,7 @@ function detectTensionIssues(project) {
3695
3705
  highRun++;
3696
3706
  if (highRun === 3) issues.push({
3697
3707
  severity: "medium",
3698
- chapters: [v.no - 2, v.no],
3708
+ chapters: runChapters(v.no - 2, v.no),
3699
3709
  item: `连续 ${highRun} 章处在高位(≥80),缺少呼吸口`,
3700
3710
  suggestion: "高潮之后补一章缓冲(关系推进、信息消化或日常),再进入下一轮对抗。"
3701
3711
  });
@@ -3708,7 +3718,7 @@ function detectTensionIssues(project) {
3708
3718
  lowRun++;
3709
3719
  if (lowRun === 5) issues.push({
3710
3720
  severity: "high",
3711
- chapters: [v.no - 4, v.no],
3721
+ chapters: runChapters(v.no - 4, v.no),
3712
3722
  item: `连续 ${lowRun} 章张力低于 40,推进可能偏慢`,
3713
3723
  suggestion: "提前兑现一个目标、抛出一个新风险,或让配角线产生冲撞。"
3714
3724
  });
@@ -4413,6 +4423,8 @@ const NOVEL_API = {
4413
4423
  usageReset: "/api/dsh-novel-forge/usage/reset",
4414
4424
  /** 章节历史版本(快照)。 */
4415
4425
  snapshot: "/api/dsh-novel-forge/snapshot",
4426
+ /** 修订指令合并(POST:汇总审稿/时间线/张力为一轮)。 */
4427
+ revisionPlan: "/api/dsh-novel-forge/revision/plan",
4416
4428
  /** 张力曲线(GET 数据 / POST 设置)。 */
4417
4429
  tension: "/api/dsh-novel-forge/tension",
4418
4430
  /** 提示词槽位(GET 列表 / POST 写入)。 */
@@ -9293,6 +9305,204 @@ function defaultOutputDirFor(bookName) {
9293
9305
  const clean = bookName.replace(/[\\/:*?"<>|]/g, "").trim().slice(0, 40) || "未命名小说";
9294
9306
  return join(homedir(), ".dsh", "novels", clean);
9295
9307
  }
9308
+ const REVISION_SOURCE_LABELS = {
9309
+ review: "审稿",
9310
+ timeline: "时间线",
9311
+ tension: "张力"
9312
+ };
9313
+ /** 固定优先级:数值越小越先改。 */
9314
+ function revisionPriority(source, severity) {
9315
+ if (source === "review") return severity === "high" ? 0 : severity === "medium" ? 3 : 4;
9316
+ if (source === "timeline") return severity === "high" ? 1 : 3;
9317
+ return severity === "high" ? 2 : 2;
9318
+ }
9319
+ /** 同优先级的来源次序:审稿 > 时间线 > 张力(同级时先改让本章"判不过"的那一类)。 */
9320
+ const SOURCE_RANK = {
9321
+ review: 0,
9322
+ timeline: 1,
9323
+ tension: 2
9324
+ };
9325
+ /**
9326
+ * 条目在**来源内部**的原始序号(id 末段)。
9327
+ * 必须按数值比较:字符串比较会得到 0,1,10,11,2… 的顺序,
9328
+ * 于是截断时先丢「第 3 条意见」却留下「第 11 条」——等于把靠前的意见丢了。
9329
+ */
9330
+ function sequenceOf(item) {
9331
+ const tail = item.id.slice(item.id.lastIndexOf(":") + 1);
9332
+ const n = Number.parseInt(tail, 10);
9333
+ return Number.isFinite(n) ? n : 0;
9334
+ }
9335
+ /** 去重键:同源 + 归一化问题描述(忽略空白与标点差异)。 */
9336
+ function dedupeKey(item) {
9337
+ return `${item.source}|${item.item.replace(/[\s,。、;:,.!?!?"'"'()()【】\[\]]/g, "").slice(0, 60)}`;
9338
+ }
9339
+ /** 排序 + 去重 + 截断。去重时保留优先级更高的那条,并累加 count。 */
9340
+ function mergeRevisionItems(items, cap = 8) {
9341
+ const sorted = [...items].sort((a, b) => {
9342
+ const byPriority = revisionPriority(a.source, a.severity) - revisionPriority(b.source, b.severity);
9343
+ if (byPriority !== 0) return byPriority;
9344
+ if (a.chapterNo !== b.chapterNo) return a.chapterNo - b.chapterNo;
9345
+ if (a.source !== b.source) return SOURCE_RANK[a.source] - SOURCE_RANK[b.source];
9346
+ const bySequence = sequenceOf(a) - sequenceOf(b);
9347
+ if (bySequence !== 0) return bySequence;
9348
+ return a.id.localeCompare(b.id);
9349
+ });
9350
+ const seen = /* @__PURE__ */ new Map();
9351
+ const unique = [];
9352
+ for (const item of sorted) {
9353
+ const key = dedupeKey(item);
9354
+ const dup = seen.get(key);
9355
+ if (dup !== void 0) {
9356
+ dup.count = (dup.count ?? 1) + 1;
9357
+ continue;
9358
+ }
9359
+ seen.set(key, item);
9360
+ unique.push(item);
9361
+ }
9362
+ const kept = unique.slice(0, cap);
9363
+ return {
9364
+ items: kept,
9365
+ omitted: Math.max(0, unique.length - kept.length)
9366
+ };
9367
+ }
9368
+ /** 时间线/张力问题是否落在目标章上(chapters 为空视为全书级,跟随目标章)。 */
9369
+ function matchesChapter(chapters, target) {
9370
+ if (target === void 0) return true;
9371
+ const list = chapters ?? [];
9372
+ if (list.length === 0) return true;
9373
+ return list.includes(target);
9374
+ }
9375
+ function pickChapterNo(chapters, target, fallback) {
9376
+ if (target !== void 0 && matchesChapter(chapters, target)) return target;
9377
+ return chapters?.[0] ?? fallback;
9378
+ }
9379
+ function timelineToItems(issues, target) {
9380
+ const out = [];
9381
+ issues.forEach((issue, i) => {
9382
+ if (!matchesChapter(issue.chapters, target)) return;
9383
+ out.push({
9384
+ id: `timeline:${i}:${pickChapterNo(issue.chapters, target, 0)}`,
9385
+ source: "timeline",
9386
+ severity: issue.severity,
9387
+ chapterNo: pickChapterNo(issue.chapters, target, 0),
9388
+ item: issue.item,
9389
+ suggestion: issue.suggestion
9390
+ });
9391
+ });
9392
+ return out;
9393
+ }
9394
+ function tensionToItems(issues, target) {
9395
+ const out = [];
9396
+ issues.forEach((issue, i) => {
9397
+ if (!matchesChapter(issue.chapters, target)) return;
9398
+ out.push({
9399
+ id: `tension:${i}:${pickChapterNo(issue.chapters, target, 0)}`,
9400
+ source: "tension",
9401
+ severity: issue.severity,
9402
+ chapterNo: pickChapterNo(issue.chapters, target, 0),
9403
+ item: issue.item,
9404
+ suggestion: issue.suggestion
9405
+ });
9406
+ });
9407
+ return out;
9408
+ }
9409
+ /** 汇总一章(或全书)的三类建议。纯函数,不改项目状态。 */
9410
+ function collectRevisionItems(project, options = {}) {
9411
+ const target = options.chapterNo;
9412
+ const items = [];
9413
+ const chapters = (project.chapters ?? []).filter((c) => target === void 0 || c.no === target);
9414
+ const explicit = options.reviewIssues;
9415
+ if (explicit !== void 0) explicit.forEach((issue, i) => {
9416
+ items.push({
9417
+ id: `review:${target ?? 0}:${i}`,
9418
+ source: "review",
9419
+ severity: issue.severity,
9420
+ chapterNo: target ?? 0,
9421
+ item: issue.item,
9422
+ suggestion: issue.suggestion
9423
+ });
9424
+ });
9425
+ else for (const chapter of chapters) for (const [i, issue] of (chapter.review?.issues ?? []).entries()) items.push({
9426
+ id: `review:${chapter.no}:${i}`,
9427
+ source: "review",
9428
+ severity: issue.severity,
9429
+ chapterNo: chapter.no,
9430
+ item: issue.item,
9431
+ suggestion: issue.suggestion
9432
+ });
9433
+ if (options.includeTimeline ?? true) items.push(...timelineToItems(detectTimelineIssues(project), target));
9434
+ if (options.includeTension ?? true) items.push(...tensionToItems(detectTensionIssues(project), target));
9435
+ return items;
9436
+ }
9437
+ /** 渲染修订指令。格式与历史「按审稿意见修订」保持同族,模型无需重新学习。 */
9438
+ function renderRevisionInstruction(items, title = "按合并建议修订") {
9439
+ const counts = {
9440
+ review: 0,
9441
+ timeline: 0,
9442
+ tension: 0
9443
+ };
9444
+ for (const item of items) counts[item.source] += item.count ?? 1;
9445
+ return `${title}${`(审稿 ${counts.review} · 时间线 ${counts.timeline} · 张力 ${counts.tension})`}:\n${items.map((item) => {
9446
+ const tag = `【${REVISION_SOURCE_LABELS[item.source]}·${item.severity}】`;
9447
+ const where = item.chapterNo > 0 ? `第${item.chapterNo}章 ` : "";
9448
+ const repeat = (item.count ?? 1) > 1 ? `(合并同类 ${item.count} 条)` : "";
9449
+ const suggestion = item.suggestion.trim() === "" ? "" : `\n建议:${item.suggestion.trim()}`;
9450
+ return `${tag}${where}${item.item}${repeat}${suggestion}`;
9451
+ }).join("\n")}`;
9452
+ }
9453
+ /**
9454
+ * 构造验证基准:把时间线/张力条目转成「上一轮意见」的编号项,
9455
+ * 与审稿意见合并成一份报告。复核模型据此逐条核对,避免
9456
+ * 「审稿说已解决、时间线说没改」这类互相打架的判定。
9457
+ */
9458
+ function buildRevisionBaseline(items, base) {
9459
+ const issues = items.map((item) => ({
9460
+ severity: item.severity,
9461
+ item: item.source === "review" ? item.item : `[${REVISION_SOURCE_LABELS[item.source]}] ${item.item}`,
9462
+ suggestion: item.suggestion
9463
+ }));
9464
+ return {
9465
+ score: base?.score ?? 0,
9466
+ passed: false,
9467
+ verdict: base?.verdict ?? "本轮修订基准",
9468
+ issues,
9469
+ riskScore: base?.riskScore,
9470
+ aiFlavor: base?.aiFlavor,
9471
+ aiPhrases: base?.aiPhrases,
9472
+ reviewedAt: base?.reviewedAt ?? (/* @__PURE__ */ new Date()).toISOString()
9473
+ };
9474
+ }
9475
+ /** 汇总 → 排序 → 指令 → 基准。面板与生产单都走这里。 */
9476
+ function buildRevisionPlan(project, options = {}) {
9477
+ const merged = mergeRevisionItems(collectRevisionItems(project, options), options.cap ?? 8);
9478
+ const counts = {
9479
+ review: 0,
9480
+ timeline: 0,
9481
+ tension: 0
9482
+ };
9483
+ for (const item of merged.items) counts[item.source] += item.count ?? 1;
9484
+ return {
9485
+ items: merged.items,
9486
+ instruction: renderRevisionInstruction(merged.items, options.title),
9487
+ counts,
9488
+ omitted: merged.omitted,
9489
+ baseline: buildRevisionBaseline(merged.items, options.baseReport)
9490
+ };
9491
+ }
9492
+ /** 一行摘要,用于日志与面板提示("审稿 3 · 时间线 1 · 张力 1,另截断 2 条")。 */
9493
+ function describeRevisionPlan(plan) {
9494
+ const parts = [];
9495
+ if (plan.counts.review > 0) parts.push(`审稿 ${plan.counts.review}`);
9496
+ if (plan.counts.timeline > 0) parts.push(`时间线 ${plan.counts.timeline}`);
9497
+ if (plan.counts.tension > 0) parts.push(`张力 ${plan.counts.tension}`);
9498
+ const head = parts.length === 0 ? "无可修订条目" : parts.join(" · ");
9499
+ return plan.omitted > 0 ? `${head}(另截断 ${plan.omitted} 条)` : head;
9500
+ }
9501
+ /** 待办文本(生产单只提示不阻塞时写入项目待办,供作者人工决定)。 */
9502
+ function revisionTodoText(item) {
9503
+ const where = item.chapterNo > 0 ? `第${item.chapterNo}章` : "全书";
9504
+ return `[${REVISION_SOURCE_LABELS[item.source]}] ${where}:${item.item}`;
9505
+ }
9296
9506
  //#endregion
9297
9507
  //#region src/run.ts
9298
9508
  /**
@@ -9480,6 +9690,51 @@ var ProductionRunner = class {
9480
9690
  chapter.status = "pending";
9481
9691
  await this.produce(project, chapter);
9482
9692
  }
9693
+ this.auditChapterAdvisories(chapter.no);
9694
+ }
9695
+ /**
9696
+ * 出章即核对曲线(建议性,不阻塞):
9697
+ * 规则初筛得到的时间线矛盾 + 张力曲线偏差写进运行日志;本章已完结时再落成项目待办。
9698
+ * 默认**不**触发任何自动修订——要看/要改由作者在面板发起「一键按建议修订」;
9699
+ * 只有当本章因审稿 high 本来就要修订时,这些问题才搭同一轮车(见 handleRejected),
9700
+ * 因此不额外增加轮次,也不会出现「改完甲又违反乙」。
9701
+ */
9702
+ auditChapterAdvisories(chapterNo) {
9703
+ const config = this.deps.getConfig();
9704
+ const outputDir = this.bookDir ?? config.outputDir;
9705
+ const project = loadProject(outputDir);
9706
+ if (project === void 0) return;
9707
+ const chapter = project.chapters.find((c) => c.no === chapterNo);
9708
+ if (chapter === void 0) return;
9709
+ const plan = buildRevisionPlan(project, {
9710
+ chapterNo,
9711
+ reviewIssues: [],
9712
+ includeTimeline: true,
9713
+ includeTension: true
9714
+ });
9715
+ if (plan.items.length === 0) return;
9716
+ this.log(`第${chapterNo}章 曲线/时间线核对:${describeRevisionPlan(plan)}——建议性提示,未自动改动`);
9717
+ if (chapter.status !== "approved") return;
9718
+ if (plan.counts.timeline === 0 && plan.counts.tension === 0) return;
9719
+ const todos = project.todos ??= [];
9720
+ const added = [];
9721
+ for (const item of plan.items.slice(0, 3)) {
9722
+ const text = revisionTodoText(item);
9723
+ if (todos.some((t) => t.text === text)) continue;
9724
+ todos.unshift({
9725
+ id: `td-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
9726
+ text,
9727
+ source: "risk",
9728
+ done: false,
9729
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
9730
+ });
9731
+ added.push(text);
9732
+ }
9733
+ if (added.length > 0) {
9734
+ project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
9735
+ saveProject(outputDir, project);
9736
+ this.log(`第${chapterNo}章 已登记 ${added.length} 条建议性待办(可在时间线/张力面板一键按建议修订)`);
9737
+ }
9483
9738
  }
9484
9739
  /** 完整质量门:生成 → 摘要+事实 → 伏笔标记 → 审稿 → 作者复盘。 */
9485
9740
  async produce(project, chapter) {
@@ -9560,7 +9815,8 @@ var ProductionRunner = class {
9560
9815
  const config = getConfig();
9561
9816
  const outputDir = this.bookDir ?? config.outputDir;
9562
9817
  const no = chapter.no;
9563
- const highs = (chapter.review?.issues ?? []).filter((i) => i.severity === "high");
9818
+ const report = chapter.review;
9819
+ const highs = (report?.issues ?? []).filter((i) => i.severity === "high");
9564
9820
  if (highs.length === 0) {
9565
9821
  chapter.status = "approved";
9566
9822
  project.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -9569,9 +9825,15 @@ var ProductionRunner = class {
9569
9825
  this.log(`第${no}章 豁免通过(无 high)`);
9570
9826
  return;
9571
9827
  }
9572
- this.log(`第${no}章 修订(${highs.length} 个 high)…`);
9828
+ let plan = buildRevisionPlan(project, {
9829
+ chapterNo: no,
9830
+ reviewIssues: highs,
9831
+ baseReport: report
9832
+ });
9833
+ this.log(`第${no}章 修订(${describeRevisionPlan(plan)})…`);
9834
+ if (plan.omitted > 0) this.log(`第${no}章 修订指令已截断 ${plan.omitted} 条(超出单轮上限,优先级低者留待下一轮)`);
9573
9835
  for (let round = 1; round <= 2; round++) {
9574
- const instr = "按审稿意见修订(优先处理):\n" + highs.map((h) => `[${h.severity}] ${h.item}${h.suggestion !== "" ? `\n建议:${h.suggestion}` : ""}`).join("\n\n");
9836
+ const instr = plan.instruction;
9575
9837
  try {
9576
9838
  for await (const _step of rewriteChapterStream(ctx, config, project, outputDir, no, instr, void 0));
9577
9839
  } catch (error) {
@@ -9585,7 +9847,7 @@ var ProductionRunner = class {
9585
9847
  this.log(`第${no}章 第${round}轮草稿缺失,重试`);
9586
9848
  continue;
9587
9849
  }
9588
- const verify = await reviewChapterText(ctx, config, fresh, draft, chapter.review);
9850
+ const verify = await reviewChapterText(ctx, config, fresh, draft, plan.baseline);
9589
9851
  const highs2 = (verify.issues ?? []).filter((i) => i.severity === "high");
9590
9852
  if (verify.passed || highs2.length === 0) {
9591
9853
  this.applyDraft(fresh, freshCh, draft, verify);
@@ -9594,9 +9856,14 @@ var ProductionRunner = class {
9594
9856
  return;
9595
9857
  }
9596
9858
  this.log(`第${no}章 第${round}轮仍不过(${highs2.length} high)`);
9859
+ plan = buildRevisionPlan(fresh ?? project, {
9860
+ chapterNo: no,
9861
+ reviewIssues: highs2,
9862
+ baseReport: verify
9863
+ });
9597
9864
  }
9598
9865
  if (this.state !== null) this.state.pendingManual.push(no);
9599
- this.log(`第${no}章 两轮修订仍不过 → 保留草稿待人工`);
9866
+ this.log(`第${no}章 2 轮修订仍不过 → 保留草稿待人工`);
9600
9867
  }
9601
9868
  applyDraft(project, chapter, draft, report) {
9602
9869
  const config = this.deps.getConfig();
@@ -13046,6 +13313,49 @@ function makeRoutes(deps) {
13046
13313
  }
13047
13314
  }
13048
13315
  };
13316
+ /** 修订指令合并:POST { op: 'plan', chapterNo, reviewIssues? } → 一份合并指令 + 验证基准。
13317
+ * 面板与生产单共用,保证「同一章只改一轮」;张力/时间线问题默认只是建议(advisory)。 */
13318
+ const revisionPlanRoute = {
13319
+ kind: "exact",
13320
+ path: NOVEL_API.revisionPlan,
13321
+ handler: async (req, res) => {
13322
+ if (!guard(req, res, "POST")) return;
13323
+ const config = getConfig();
13324
+ const body = await readJsonBody(req);
13325
+ const project = loadProject(resolveOutputDir(config, body?.bookId));
13326
+ if (project === void 0) {
13327
+ writeJson(res, 400, { error: "输出目录中没有项目" });
13328
+ return;
13329
+ }
13330
+ const chapterNo = body?.chapterNo;
13331
+ if (chapterNo !== void 0 && !project.chapters.some((c) => c.no === chapterNo)) {
13332
+ writeJson(res, 404, { error: `章节 ${String(chapterNo)} 不在计划中` });
13333
+ return;
13334
+ }
13335
+ const chapter = chapterNo === void 0 ? void 0 : project.chapters.find((c) => c.no === chapterNo);
13336
+ try {
13337
+ const plan = buildRevisionPlan(project, {
13338
+ chapterNo,
13339
+ reviewIssues: body?.reviewIssues,
13340
+ includeTimeline: body?.includeTimeline,
13341
+ includeTension: body?.includeTension,
13342
+ baseReport: chapter?.review,
13343
+ cap: 8
13344
+ });
13345
+ writeJson(res, 200, {
13346
+ chapterNo: chapterNo ?? 0,
13347
+ items: plan.items,
13348
+ instruction: plan.instruction,
13349
+ counts: plan.counts,
13350
+ omitted: plan.omitted,
13351
+ baseline: plan.baseline,
13352
+ summary: describeRevisionPlan(plan)
13353
+ });
13354
+ } catch (error) {
13355
+ writeJson(res, 500, { error: error.message });
13356
+ }
13357
+ }
13358
+ };
13049
13359
  /** 章节历史版本:GET 列表;POST op=create|restore|remove|prune。 */
13050
13360
  const snapshotRoute = {
13051
13361
  kind: "exact",
@@ -14928,6 +15238,7 @@ function makeRoutes(deps) {
14928
15238
  llmPromptRoute,
14929
15239
  usageResetRoute,
14930
15240
  timelineRoute,
15241
+ revisionPlanRoute,
14931
15242
  snapshotRoute,
14932
15243
  tensionRoute,
14933
15244
  promptSlotsRoute,
@@ -15113,7 +15424,7 @@ const DEFAULT_AUTO_REVIEW_AFTER_REVISE = true;
15113
15424
  /** Order of the announcement section within the tool-guidance band. */
15114
15425
  const SECTION_ORDER = 160;
15115
15426
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
15116
- const NOVEL_GUIDANCE = "本机已安装 dsh-novel-forge 插件(AI 编译小说工作台):侧边栏「小说工坊」入口。能力:读取 docx 大纲或粘贴大纲文本;用 LLM 提炼道藏(人设/世界观/金手指规则/写作红线);生成卷计划与章节计划;逐章调用 LLM 生成 3000-4000 字正文并保存为 Markdown(默认输出到用户主目录 ~/.dsh/novels);每章自动生成摘要(叙事记忆)、自动 AI 审稿(人设/设定/红线/文笔/爽点/逻辑),支持按审稿意见重写、去 AI 味润色、暗线(伏笔)管理、批量连写与全本导出(txt/md)。限制:生成消耗 LLM API 额度;输出目录与模型可在插件设置中修改;章节正文质量取决于大纲完整度。用户提到「小说 / 大纲 / 写小说 / 章节 / 审稿 / 润色」时即指本插件,请据此协作。";
15427
+ const NOVEL_GUIDANCE = "本机已安装 dsh-novel-forge 插件(AI 编译小说工作台):侧边栏「小说工坊」入口。能力:读取 docx 大纲、粘贴大纲文本或把 txt 全本拆章导入;用 LLM 提炼道藏(人设/世界观/金手指规则/写作红线)与大世界(境界/区域/势力);生成卷计划与结构化章节计划(本章目标/剧情要点/必达项/义务合约/章末钩子);逐章生成 3000-4000 字正文并保存为 Markdown(默认输出到用户主目录 ~/.dsh/novels);每章自动生成摘要与编年录事实、抽取故事时间线(故事内时间/地点/在场角色/事件)、自动 AI 审稿(人设/设定/红线/文笔/爽点/逻辑/反 AI/呈现/合规九维)与作者复盘;支持按审稿意见合并修订(审稿 high + 时间线矛盾 + 张力偏差合成一轮,每章最多两轮后转人工)、去 AI 味润色、章节历史版本回滚、暗线(伏笔)管理、剧情线与角色库、张力曲线、全书一致性质检与敏感词扫描、分范围导出与项目备份、批量连写(生产单,可暂停/续跑)。限制:所有 AI 操作消耗 LLM API 额度;输出目录与模型可在插件设置中修改;章节正文质量取决于大纲完整度;张力/时间线问题默认只是建议(写日志与待办),不会自动改正文。用户提到「小说 / 大纲 / 写小说 / 章节 / 审稿 / 润色」时即指本插件,请据此协作。";
15117
15428
  /** Resolve a config-like value into the full runtime config. */
15118
15429
  function resolveConfig(value) {
15119
15430
  return {
@@ -3,7 +3,7 @@
3
3
  * fetch, same origin; generation/rewrite/polish ride NDJSON streams read
4
4
  * incrementally.
5
5
  */
6
- import { type AssetsPatch, type AssetsResponse, type AddModelRequest, type AddModelResponse, type LlmModelsResponse, type LlmProvidersResponse, type LlmVendorsResponse, type RemoveProviderRequest, type RemoveProviderResponse, type BibleResponse, type ChapterResponse, type ConfigPatch, type ExportResponse, type ForeshadowRequest, type ForeshadowResponse, type JobFrame, type LoadOutlineResponse, type AdaptExecuteRequest, type AdaptRewriteFrame, type AdaptMaterializeSaveRequest, type AdaptMaterializeSaveResponse, type PluginUpdateResponse, type LlmTestResponse, type NovelConfig, type PlanResponse, type ReviewReport, type StatusResponse, type StyleEngineRequest, type StyleAsset, type VolumesResponse } from '../protocol.ts';
6
+ import { type AssetsPatch, type AssetsResponse, type AddModelRequest, type AddModelResponse, type LlmModelsResponse, type LlmProvidersResponse, type LlmVendorsResponse, type RemoveProviderRequest, type RemoveProviderResponse, type BibleResponse, type ChapterResponse, type ConfigPatch, type ExportResponse, type ForeshadowRequest, type ForeshadowResponse, type JobFrame, type LoadOutlineResponse, type AdaptExecuteRequest, type AdaptRewriteFrame, type AdaptMaterializeSaveRequest, type AdaptMaterializeSaveResponse, type PluginUpdateResponse, type LlmTestResponse, type NovelConfig, type PlanResponse, type ReviewReport, type RevisionPlanResponse, type StatusResponse, type StyleEngineRequest, type StyleAsset, type VolumesResponse } from '../protocol.ts';
7
7
  /** Error carrying the route's JSON error message. */
8
8
  export declare class NovelApiError extends Error {
9
9
  constructor(message: string);
@@ -39,6 +39,14 @@ export declare class NovelApi {
39
39
  chapterCheck(no: number, text: string, previousReport?: ReviewReport): Promise<{
40
40
  report: ReviewReport;
41
41
  }>;
42
+ /**
43
+ * 修订指令合并:把本章的审稿意见(勾选项)+ 时间线矛盾 + 张力偏差合成**一份**指令。
44
+ * 面板所有「按意见修订 / 一键按建议修订」入口都先走这里,保证同一章只改一轮。
45
+ */
46
+ revisionPlan(no: number, reviewIssues?: ReviewReport['issues'], opts?: {
47
+ includeTimeline?: boolean;
48
+ includeTension?: boolean;
49
+ }): Promise<RevisionPlanResponse>;
42
50
  /** 保存手动编辑的正文(自动备份 .bak;带报告则沿用落盘,否则保存后自动审稿)。 */
43
51
  chapterSave(no: number, text: string, report?: ReviewReport): Promise<import('../protocol.ts').ChapterSaveResponse>;
44
52
  patchConfig(patch: ConfigPatch): Promise<{
@@ -1,57 +1,36 @@
1
1
  /**
2
- * dsh-novel-forge — locale dictionaries (zh / en).
2
+ * dsh-novel-forge — 面板文案词典(简体中文单语)。
3
+ *
4
+ * 定位说明:小说工坊面板是**中文单语界面**——这里就是全部用户可见文案的唯一定义处,
5
+ * 键位即类型(`NovelKey`),新增文案请同时在此登记,不要散落到组件里硬编码。
6
+ * 文档层(README / docs / CHANGELOG)仍提供英文对照,那是给人读的,不是界面语言。
3
7
  */
4
- /** zh dictionary. */
8
+ /** 面板文案(简体中文)。 */
5
9
  export declare const zh: {
6
10
  readonly 'entry.label': "小说工坊";
7
11
  readonly 'entry.tooltip': "AI 编译小说工作台:总纲 → 道藏 → 卷计划 → 章节计划 → 逐章生成+审稿";
8
- readonly 'panel.title': "小说工坊";
9
12
  readonly 'common.close': "关闭";
10
13
  readonly 'common.loading': "加载中…";
11
- readonly 'common.save': "保存";
12
14
  readonly 'common.error': "错误";
13
- readonly 'common.success': "成功";
14
- readonly 'common.generating': "生成中…";
15
15
  readonly 'common.chars': "字";
16
16
  readonly 'tab.workflow': "工作台";
17
17
  readonly 'tab.overview': "总纲";
18
18
  readonly 'tab.plan': "章节";
19
- readonly 'tab.bible': "道藏";
20
- readonly 'tab.foreshadow': "暗线";
21
19
  readonly 'tab.assistant': "AI 编辑 Agent";
22
20
  readonly 'tab.settings': "设置";
23
- readonly 'workflow.title': "创作工作台";
24
- readonly 'workflow.step1': "① 加载总纲";
25
- readonly 'workflow.step2': "② 提炼道藏";
26
- readonly 'workflow.step3': "③ 规划卷";
27
- readonly 'workflow.step4': "④ 生成章节计划";
28
- readonly 'workflow.step5': "⑤ 逐章写作 + AI 审稿";
29
- readonly 'workflow.step6': "⑥ 润色 / 导出";
30
- readonly 'workflow.loadOutline': "读取总纲";
31
- readonly 'workflow.genBible': "提炼道藏";
32
21
  readonly 'workflow.genVolumes': "生成卷计划";
33
- readonly 'workflow.genPlan': "生成章节计划";
34
- readonly 'workflow.done': "已完成";
35
- readonly 'workflow.todo': "待办";
36
22
  readonly 'workflow.bibleDone': "道藏已生成({n} 条规则 / {c} 个人物 / {r} 条红线)";
37
23
  readonly 'workflow.volumesDone': "卷计划已生成({n} 卷)";
38
24
  readonly 'workflow.planDone': "章节计划已生成({n} 章)";
39
- readonly 'workflow.progress': "进度:总纲 ✓ · 道藏 {bible} · 卷 {volumes} · 计划 {plan} · 已完成 {done}/{total} 章";
40
- readonly 'overview.loadDocx': "从 docx 读取总纲";
41
- readonly 'overview.loadDocxDefault': "读取默认总纲";
42
25
  readonly 'overview.loadingOutline': "正在解析 docx…";
43
26
  readonly 'overview.outlineHint': "总纲文本(可编辑)";
44
27
  readonly 'overview.outlineChars': "总纲字数";
45
28
  readonly 'overview.saveOutline': "保存总纲";
46
29
  readonly 'overview.saved': "总纲已保存";
47
30
  readonly 'overview.bookName': "书名";
48
- readonly 'overview.loadCustom': "指定 docx 路径";
49
- readonly 'overview.loadCustomHint': "绝对路径,留空使用默认";
50
31
  readonly 'plan.generate': "生成章节计划";
51
32
  readonly 'plan.generateHint': "章节数量";
52
33
  readonly 'plan.count': "章";
53
- readonly 'plan.empty': "暂无章节计划,请先生成";
54
- readonly 'plan.chapter': "章";
55
34
  readonly 'plan.pending': "待生成";
56
35
  readonly 'plan.generating': "生成中";
57
36
  readonly 'plan.written': "待审稿";
@@ -63,21 +42,15 @@ export declare const zh: {
63
42
  readonly 'plan.rewrite': "修订";
64
43
  readonly 'plan.review': "审稿";
65
44
  readonly 'plan.polish': "去AI味";
66
- readonly 'plan.writeAll': "批量生成全部";
67
45
  readonly 'plan.writeAllPending': "批量生成剩余";
68
- readonly 'plan.generated': "已生成";
69
- readonly 'plan.progress': "进度";
70
46
  readonly 'plan.beats': "剧情要点";
71
47
  readonly 'plan.reviewReport': "审稿报告";
72
48
  readonly 'plan.reviewScore': "评分";
73
49
  readonly 'plan.reviewVerdict': "总评";
74
- readonly 'plan.reviewIssues': "问题清单";
75
50
  readonly 'plan.reviewPass': "通过";
76
51
  readonly 'plan.reviewFail': "未通过";
77
- readonly 'plan.approve': "手动通过";
78
52
  readonly 'plan.summary': "章节摘要";
79
53
  readonly 'plan.volumes': "卷";
80
- readonly 'plan.noVolume': "未分卷";
81
54
  readonly 'bible.title': "道藏";
82
55
  readonly 'bible.gen': "提炼道藏";
83
56
  readonly 'bible.genre': "题材基调";
@@ -89,7 +62,6 @@ export declare const zh: {
89
62
  readonly 'foreshadow.title': "暗线管理";
90
63
  readonly 'foreshadow.suggest': "建议暗线";
91
64
  readonly 'foreshadow.none': "暂无暗线";
92
- readonly 'foreshadow.status': "状态";
93
65
  readonly 'foreshadow.planned': "计划中";
94
66
  readonly 'foreshadow.planted': "已埋设";
95
67
  readonly 'foreshadow.progressing': "推进中";
@@ -99,7 +71,6 @@ export declare const zh: {
99
71
  readonly 'foreshadow.plantedAt': "埋设于";
100
72
  readonly 'foreshadow.setPlanted': "标记已埋设";
101
73
  readonly 'foreshadow.setResolved': "标记已回收";
102
- readonly 'tab.facts': "编年录";
103
74
  readonly 'tab.plotlines': "剧情线";
104
75
  readonly 'plotlines.hint': "主线/支线/人物线/悬念线的推进管理:章节生成时会注入未完结的线,要求每章至少推进一条;关联章节记录推进节点。";
105
76
  readonly 'plotlines.empty': "还没有剧情线——长篇小说建议先建主线与几条支线。";
@@ -121,11 +92,7 @@ export declare const zh: {
121
92
  readonly 'plotlines.cancel': "取消";
122
93
  readonly 'plotlines.edit': "编辑";
123
94
  readonly 'plotlines.remove': "删除";
124
- readonly 'plotlines.linkChapter': "关联本章";
125
95
  readonly 'plotlines.chapters': "已关联";
126
- readonly 'plotlines.workflowTitle': "剧情线进度";
127
- readonly 'plotlines.workflowEmpty': "未建剧情线(去「剧情线」页创建)";
128
- readonly 'sensitive.title': "敏感词检查";
129
96
  readonly 'sensitive.scanAll': " 全书敏感词检查";
130
97
  readonly 'sensitive.hint': "内置违禁词库硬匹配(政治/擦边/暴力/辱骂/广告/其他),发布前建议检查;只做提示,是否修改由你判断。";
131
98
  readonly 'sensitive.clean': " 未命中任何违禁词(扫描 {n} 章)";
@@ -135,9 +102,6 @@ export declare const zh: {
135
102
  readonly 'facts.title': "编年录 / 时间线({n} 条)";
136
103
  readonly 'facts.hint': "每章生成后自动抽取「已确立事实」(人物状态/境界资源/关系变化/伏笔落地),最近 20 条注入后续章节生成,保证长期一致。";
137
104
  readonly 'facts.backfill': "回填历史章节事实";
138
- readonly 'facts.backfilled': "已从历史章节回填 {n} 条事实";
139
- readonly 'settings.title': "设置";
140
- readonly 'settings.outlinePath': "默认大纲路径";
141
105
  readonly 'settings.outputDir': "输出目录";
142
106
  readonly 'settings.provider': "模型提供商";
143
107
  readonly 'settings.model': "模型";
@@ -146,13 +110,7 @@ export declare const zh: {
146
110
  readonly 'settings.edit': "编辑";
147
111
  readonly 'settings.delete': "删除";
148
112
  readonly 'settings.modelCustomPlaceholder': "输入任意模型 id";
149
- readonly 'settings.savedModels': "我的模型";
150
- readonly 'settings.modelCards': "模型卡片(内置 + 已添加)";
151
- readonly 'settings.savedModelName': "名称";
152
- readonly 'settings.addModel': "添加模型";
153
- readonly 'settings.catalogRefresh': "刷新厂商";
154
113
  readonly 'settings.refreshModels': "刷新模型";
155
- readonly 'settings.savedModelsHint': "手动添加常用模型(provider + 模型 id),点击即可切换当前模型;厂商模式会写入 DSH 凭据并注册路由。";
156
114
  readonly 'settings.testConnection': "测试连通";
157
115
  readonly 'settings.testRunning': "测试中";
158
116
  readonly 'settings.testOk': "连通";
@@ -167,15 +125,9 @@ export declare const zh: {
167
125
  readonly 'settings.reasoning.low': "low · 低思考";
168
126
  readonly 'settings.reasoning.high': "high · 高思考";
169
127
  readonly 'settings.reasoning.max': "max · 最高思考(质检/推演)";
170
- readonly 'settings.theme': "主题";
171
- readonly 'settings.themeLiquid': "iOS 液态玻璃(绿)";
172
- readonly 'settings.themeNeumorph': "新拟物(双阴影)";
173
- readonly 'settings.themeHint': "存于浏览器本地,即时生效";
174
128
  readonly 'settings.chapterChars': "每章目标字数";
175
129
  readonly 'settings.maxTokens': "单章最大输出 tokens";
176
130
  readonly 'settings.reviewPassScore': "审稿通过分数(0-100)";
177
- readonly 'settings.editorFontSize': "编辑器字号(仅编辑页显示)";
178
- readonly 'settings.editorFontSizeHint': "存于浏览器本地,不影响其他设备";
179
131
  readonly 'settings.autoReview': "生成后自动审稿";
180
132
  readonly 'settings.autoAuthorReview': "生成后自动作者复盘";
181
133
  readonly 'settings.autoAuthorReviewHint': "复盘钩子兑现/结尾钩子/剧情线推进/连续性/节奏趋势,每章约 2000 token";
@@ -184,7 +136,6 @@ export declare const zh: {
184
136
  readonly 'settings.save': "保存设置";
185
137
  readonly 'settings.saved': "设置已保存";
186
138
  readonly 'settings.openFolder': "打开输出文件夹";
187
- readonly 'settings.export': "导出";
188
139
  readonly 'settings.exportTxt': "导出 TXT";
189
140
  readonly 'settings.exportMd': "导出 Markdown";
190
141
  readonly 'settings.exported': "已导出:{file}({chars} 字,{chapters} 章)";
@@ -194,19 +145,12 @@ export declare const zh: {
194
145
  readonly 'progress.error': "第 {no} 章失败:{message}";
195
146
  readonly 'progress.rewriting': "正在修订第 {no} 章…";
196
147
  readonly 'progress.polishing': "正在润色第 {no} 章…";
197
- readonly 'progress.empty': "生成/审稿进度将显示在这里";
198
148
  readonly 'assistant.hint': "和 AI 编辑 Agent 讨论剧情、人设、暗线;达成一致后可让它直接修改总纲、道藏、章节内容。";
199
149
  readonly 'assistant.placeholder': "例如:我想让第 2 章结尾加一个悬念——墟境里传来爷爷的声音…";
200
150
  readonly 'assistant.send': "发送";
201
- readonly 'assistant.toolStart': " 执行操作:{name}…";
202
- readonly 'assistant.toolDone': "✓ {name} 完成:{detail}";
203
- readonly 'assistant.toolError': "✗ {name} 失败:{detail}";
204
151
  readonly 'assistant.empty': "还没有对话。和 AI 编辑 Agent 聊聊剧情吧。";
205
152
  readonly 'status.projectNone': "输出目录中还没有项目。请先加载大纲。";
206
153
  readonly 'status.files': "已生成文件";
207
- readonly 'api.error': "请求失败";
208
154
  };
209
- /** en dictionary (fallback). */
210
- export declare const en: Record<string, string>;
211
- /** Keys type for the zh dictionary. */
155
+ /** 面板文案键位(= zh 词典的键)。 */
212
156
  export type NovelKey = keyof typeof zh;
@@ -10,5 +10,9 @@
10
10
  import type { NovelApi } from '../api.ts';
11
11
  export interface TensionViewProps {
12
12
  api: NovelApi;
13
+ /** 一键按建议修订(合并审稿/张力/时间线后改这一章;由父级统一发起)。 */
14
+ onRevise?: (chapterNo: number) => void;
15
+ /** 外部忙碌态(修订进行中时禁用按钮)。 */
16
+ busy?: boolean;
13
17
  }
14
- export default function TensionView({ api }: TensionViewProps): JSX.Element;
18
+ export default function TensionView({ api, onRevise, busy: outerBusy }: TensionViewProps): JSX.Element;
@@ -10,5 +10,9 @@ export interface TimelineViewProps {
10
10
  api: NovelApi;
11
11
  /** 章节号列表(用于「抽取本章」下拉)。 */
12
12
  chapters: number[];
13
+ /** 一键按建议修订(合并审稿/时间线/张力后改这一章;由父级统一发起)。 */
14
+ onRevise?: (chapterNo: number) => void;
15
+ /** 外部忙碌态(修订进行中时禁用按钮)。 */
16
+ busy?: boolean;
13
17
  }
14
- export default function TimelineView({ api, chapters }: TimelineViewProps): JSX.Element;
18
+ export default function TimelineView({ api, chapters, onRevise, busy: outerBusy }: TimelineViewProps): JSX.Element;