@musnows/scriverse 0.7.12 → 0.7.13

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/dist/ai.js CHANGED
@@ -63,6 +63,9 @@ const interactiveStreamErrorCodes = new Set([
63
63
  function isInteractiveStreamError(error) {
64
64
  return error instanceof AppError && interactiveStreamErrorCodes.has(error.code);
65
65
  }
66
+ function isAuthorNoteChapter(chapter) {
67
+ return String(chapter.chapterType ?? "") === "作者的话";
68
+ }
66
69
  function interactiveStreamRequestCancelledError() {
67
70
  return new AppError(499, "AI_STREAM_REQUEST_CANCELLED", "AI 流式请求已取消");
68
71
  }
@@ -1256,10 +1259,15 @@ export class ContextBuilder {
1256
1259
  if (scope.type === "selection") {
1257
1260
  if (!scope.selection)
1258
1261
  throw new AppError(400, "SELECTION_REQUIRED", "选中文本上下文不能为空");
1262
+ const selectionChapter = scope.chapterId ? this.store.getChapter(scope.chapterId) : null;
1263
+ if (selectionChapter && selectionChapter.workId !== workId)
1264
+ throw new AppError(400, "CHAPTER_WORK_MISMATCH", "章节不属于当前作品");
1259
1265
  // 分析任务会在 selection 中放入服务端 CHAPTER 标记,不能转义
1260
- contentSections.push(wrapAiContextRegion("selection", `当前选中文本:\n${scope.selection}`, { escape: false }));
1261
- if (scope.chapterId)
1262
- this.appendChapter(contentSections, workId, scope.chapterId, false);
1266
+ if (!selectionChapter || !isAuthorNoteChapter(selectionChapter)) {
1267
+ contentSections.push(wrapAiContextRegion("selection", `当前选中文本:\n${scope.selection}`, { escape: false }));
1268
+ if (scope.chapterId)
1269
+ this.appendChapter(contentSections, workId, scope.chapterId, false);
1270
+ }
1263
1271
  }
1264
1272
  else if (scope.type === "chapter") {
1265
1273
  const chapterIds = [...new Set([
@@ -1272,8 +1280,9 @@ export class ContextBuilder {
1272
1280
  this.appendPreviousChapterTail(contentSections, workId, scope.chapterId);
1273
1281
  for (const chapterId of chapterIds)
1274
1282
  this.appendChapter(contentSections, workId, chapterId, true);
1275
- if (scope.selection)
1283
+ if (scope.selection && (!scope.chapterId || !isAuthorNoteChapter(this.store.getChapter(scope.chapterId)))) {
1276
1284
  contentSections.push(wrapAiContextRegion("selection", `当前选中文本(本次修改目标):\n${scope.selection}`, { escape: false }));
1285
+ }
1277
1286
  }
1278
1287
  else if (scope.type === "volume") {
1279
1288
  const volumeIds = [...new Set([
@@ -1288,7 +1297,7 @@ export class ContextBuilder {
1288
1297
  const volume = volumes.find((item) => item.id === volumeId);
1289
1298
  if (!volume)
1290
1299
  throw notFound("卷");
1291
- const chapters = volume.chapters;
1300
+ const chapters = volume.chapters.filter((chapter) => !isAuthorNoteChapter(chapter));
1292
1301
  contentSections.push(wrapAiContextRegion("volume", `当前卷:${String(volume.title)}`));
1293
1302
  for (const chapter of chapters) {
1294
1303
  contentSections.push(wrapAiContextRegion("chapter", `[${String(volume.title)} / ${String(chapter.title)} | 版本 ${String(chapter.versionNo)}]\n${String(chapter.content)}`));
@@ -1300,7 +1309,7 @@ export class ContextBuilder {
1300
1309
  const volumes = tree.volumes;
1301
1310
  contentSections.push(wrapAiContextRegion("book", "全书正文(按问题相关度选取原文,完整结构见章节概要):"));
1302
1311
  for (const volume of volumes) {
1303
- for (const chapter of volume.chapters) {
1312
+ for (const chapter of volume.chapters.filter((item) => !isAuthorNoteChapter(item))) {
1304
1313
  contentSections.push(wrapAiContextRegion("chapter", `[# ${String(volume.title)} / ${String(chapter.title)} | 版本 ${String(chapter.versionNo)}]\n${String(chapter.content)}`));
1305
1314
  }
1306
1315
  }
@@ -1384,8 +1393,9 @@ export class ContextBuilder {
1384
1393
  if (chapter.workId !== workId)
1385
1394
  throw new AppError(400, "CHAPTER_WORK_MISMATCH", "引用章节不属于当前作品");
1386
1395
  }
1387
- if (chapters.length) {
1388
- contentSections.push(wrapAiContextRegion("referenced_chapters", `作者主动引用的章节:\n${chapters
1396
+ const eligibleChapters = chapters.filter((chapter) => !isAuthorNoteChapter(chapter));
1397
+ if (eligibleChapters.length) {
1398
+ contentSections.push(wrapAiContextRegion("referenced_chapters", `作者主动引用的章节:\n${eligibleChapters
1389
1399
  .map((chapter) => `[${String(chapter.title)} | 版本 ${String(chapter.versionNo)}]\n${String(chapter.content)}`)
1390
1400
  .join("\n\n")}`));
1391
1401
  }
@@ -1490,6 +1500,8 @@ export class ContextBuilder {
1490
1500
  const chapter = this.store.getChapter(chapterId);
1491
1501
  if (chapter.workId !== workId)
1492
1502
  throw new AppError(400, "CHAPTER_WORK_MISMATCH", "章节不属于当前作品");
1503
+ if (isAuthorNoteChapter(chapter))
1504
+ return;
1493
1505
  sections.push(wrapAiContextRegion("chapter", includeContent
1494
1506
  ? `当前章节:${String(chapter.title)} | 版本 ${String(chapter.versionNo)}\n${String(chapter.content)}`
1495
1507
  : `所在章节:${String(chapter.title)} | 版本 ${String(chapter.versionNo)}`));
@@ -1503,7 +1515,7 @@ export class ContextBuilder {
1503
1515
  return;
1504
1516
  const perVolumeBudget = Math.max(24, Math.floor(maximumTokens / volumes.length));
1505
1517
  for (const volume of volumes) {
1506
- const chapters = volume.chapters;
1518
+ const chapters = volume.chapters.filter((chapter) => !isAuthorNoteChapter(chapter));
1507
1519
  const ranked = chapters.map((chapter, order) => {
1508
1520
  const summary = summaryByChapterId.get(String(chapter.id)) ?? "";
1509
1521
  const line = `- ${String(chapter.title)}:${summary || "尚无章节概要"}`;
@@ -1532,9 +1544,12 @@ export class ContextBuilder {
1532
1544
  }
1533
1545
  }
1534
1546
  appendPreviousChapterTail(sections, workId, chapterId) {
1547
+ const current = this.store.getChapter(chapterId);
1548
+ if (current.workId !== workId || isAuthorNoteChapter(current))
1549
+ return;
1535
1550
  const tree = this.store.getWorkTree(workId);
1536
1551
  const chapters = tree.volumes
1537
- .flatMap((volume) => volume.chapters);
1552
+ .flatMap((volume) => volume.chapters.filter((chapter) => !isAuthorNoteChapter(chapter)));
1538
1553
  const index = chapters.findIndex((chapter) => chapter.id === chapterId);
1539
1554
  if (index <= 0)
1540
1555
  return;
@@ -1545,6 +1560,9 @@ export class ContextBuilder {
1545
1560
  sections.push(wrapAiContextRegion("previous_chapter_tail", `上一章节结尾:${String(previous.title)} | 版本 ${String(previous.versionNo)}\n${content.slice(-5000)}`));
1546
1561
  }
1547
1562
  appendChapterKnowledge(sections, workId, chapterId) {
1563
+ const chapter = this.store.getChapter(chapterId);
1564
+ if (chapter.workId !== workId || isAuthorNoteChapter(chapter))
1565
+ return;
1548
1566
  const outline = this.store.getChapterOutline(chapterId);
1549
1567
  if (outline) {
1550
1568
  sections.push(wrapAiContextRegion("chapter_outline", `当前章大纲(创作约束):\n目标:${String(outline.goal) || "未填写"}\n冲突:${String(outline.conflict) || "未填写"}\n转折:${String(outline.turningPoint) || "未填写"}\n状态:${String(outline.status)}`));
@@ -3434,7 +3452,26 @@ export class AiManager {
3434
3452
  && titleModelId
3435
3453
  && (conversationBefore?.title === "新对话" || conversationBefore?.title === defaultTitle));
3436
3454
  const processStartedAt = process.hrtime.bigint();
3437
- const generated = await this.generate({ ...input, taskType: "chat" }, onDelta);
3455
+ let persistedConversationMessage = null;
3456
+ let streamedConversationContent = "";
3457
+ const persistStreamDelta = (delta) => {
3458
+ if (input.conversationId && input.assistantMessageRequestId && delta.length > 0) {
3459
+ streamedConversationContent += delta;
3460
+ persistedConversationMessage = this.store.upsertAiConversationAssistantMessage(input.conversationId, input.assistantMessageRequestId, streamedConversationContent);
3461
+ }
3462
+ onDelta(delta);
3463
+ };
3464
+ let generated;
3465
+ try {
3466
+ generated = await this.generate({ ...input, taskType: "chat" }, persistStreamDelta);
3467
+ }
3468
+ catch (error) {
3469
+ if (persistedConversationMessage && input.conversationId && input.assistantMessageRequestId) {
3470
+ const interruptionCode = error instanceof AppError ? error.code : "AI_STREAM_FAILED";
3471
+ persistedConversationMessage = this.store.upsertAiConversationAssistantMessage(input.conversationId, input.assistantMessageRequestId, streamedConversationContent, { interrupted: true, interruptionCode }, true);
3472
+ }
3473
+ throw error;
3474
+ }
3438
3475
  const processDurationMs = Math.min(86_400_000, Math.max(0, Math.round(Number(process.hrtime.bigint() - processStartedAt) / 1_000_000)));
3439
3476
  const chapter = input.scope.chapterId ? this.store.getChapter(input.scope.chapterId) : null;
3440
3477
  const suggestionId = id("suggestion");
@@ -3442,22 +3479,17 @@ export class AiManager {
3442
3479
  source_text, content, action, status, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, 'chat', ?, ?, ?, 'note', 'pending', ?, ?)`, suggestionId, generated.callId, input.workId, chapter ? String(chapter.id) : null, chapter ? Number(chapter.versionNo) : null, input.instruction, input.scope.selection ?? "", generated.content, now(), currentRequestActor()?.userId ?? null);
3443
3480
  const modelDisplayName = typeof generated.model.displayName === "string" ? generated.model.displayName : undefined;
3444
3481
  const conversationMessage = input.conversationId && input.assistantMessageRequestId
3445
- ? this.store.addAiConversationMessage(input.conversationId, {
3446
- role: "assistant",
3447
- content: generated.content,
3448
- requestId: input.assistantMessageRequestId,
3449
- metadata: {
3450
- ...(modelDisplayName ? { modelDisplayName } : {}),
3451
- outputTokens: generated.outputTokens,
3452
- processDurationMs,
3453
- ...(generated.reasoningContent === undefined ? {} : { reasoningContent: generated.reasoningContent }),
3454
- ...(generated.cacheHitPercent === undefined ? {} : { cacheHitPercent: generated.cacheHitPercent }),
3455
- toolCalls: generated.toolCalls,
3456
- processSteps: generated.processSteps,
3457
- ...(generated.anthropicContent?.length ? { anthropicContent: generated.anthropicContent } : {})
3458
- }
3459
- })
3460
- : null;
3482
+ ? this.store.upsertAiConversationAssistantMessage(input.conversationId, input.assistantMessageRequestId, generated.content, {
3483
+ ...(modelDisplayName ? { modelDisplayName } : {}),
3484
+ outputTokens: generated.outputTokens,
3485
+ processDurationMs,
3486
+ ...(generated.reasoningContent === undefined ? {} : { reasoningContent: generated.reasoningContent }),
3487
+ ...(generated.cacheHitPercent === undefined ? {} : { cacheHitPercent: generated.cacheHitPercent }),
3488
+ toolCalls: generated.toolCalls,
3489
+ processSteps: generated.processSteps,
3490
+ ...(generated.anthropicContent?.length ? { anthropicContent: generated.anthropicContent } : {})
3491
+ }, true)
3492
+ : persistedConversationMessage;
3461
3493
  if (shouldGenerateTitle && conversationMessage && input.conversationId) {
3462
3494
  void this.generateConversationTitle(input.workId, input.conversationId, titleModelId, firstUserContent, generated.content, defaultTitle).catch((error) => {
3463
3495
  logger.warn("ai.conversation_title.failed", { workId: input.workId, conversationId: input.conversationId, error: aiErrorForLog(error) });
@@ -4606,7 +4638,7 @@ export class AiManager {
4606
4638
  };
4607
4639
  }
4608
4640
  const args = parsed.data;
4609
- const scopedChapterIds = scope && (scope.type === "chapter" || scope.type === "volume")
4641
+ const scopedChapterIds = scope && (scope.type === "chapter" || scope.type === "volume" || scope.type === "book")
4610
4642
  ? new Set(this.getScopeChapters(workId, scope).map((chapter) => String(chapter.id)))
4611
4643
  : null;
4612
4644
  if (name === "recall_relationship") {
@@ -4820,7 +4852,7 @@ export class AiManager {
4820
4852
  .map((item) => item.trim()).filter(Boolean).slice(0, 10);
4821
4853
  const seenParagraphs = new Set();
4822
4854
  for (const identityTerm of identityTerms) {
4823
- for (const paragraph of this.store.searchChapterParagraphs(workId, identityTerm, 50)) {
4855
+ for (const paragraph of this.store.searchChapterParagraphs(workId, identityTerm, 50, { excludeAuthorNotes: true })) {
4824
4856
  const key = `${String(paragraph.chapterId)}:${String(paragraph.paragraph)}`;
4825
4857
  if (seenParagraphs.has(key))
4826
4858
  continue;
@@ -4855,7 +4887,7 @@ export class AiManager {
4855
4887
  if (name === "story_index") {
4856
4888
  const { offset, limit, cursor } = args;
4857
4889
  const work = this.store.getWork(workId);
4858
- const chapterPage = this.store.getStoryIndexChapterPage(workId, offset, limit);
4890
+ const chapterPage = this.store.getStoryIndexChapterPage(workId, offset, limit, { excludeAuthorNotes: true });
4859
4891
  const workRecords = structuralToolResultRecords([{
4860
4892
  id: work.id,
4861
4893
  title: work.title,
@@ -4914,6 +4946,8 @@ export class AiManager {
4914
4946
  const chapter = this.store.getChapter(chapterId);
4915
4947
  if (chapter.workId !== workId)
4916
4948
  return { chapterId, error: { code: "CHAPTER_WORK_MISMATCH", message: "The requested chapter belongs to a different work." } };
4949
+ if (isAuthorNoteChapter(chapter))
4950
+ return { chapterId, error: { code: "CHAPTER_AUTHOR_NOTE_EXCLUDED", message: "Author notes are excluded from AI context." } };
4917
4951
  const content = collapseAiBlankLines(String(chapter.content));
4918
4952
  return { chapterId, title: chapter.title, versionNo: chapter.versionNo, ...(include !== "content" ? { summary: summaries.get(chapterId) ?? "" } : {}), ...(include !== "summary" ? { content } : {}) };
4919
4953
  }
@@ -4931,7 +4965,7 @@ export class AiManager {
4931
4965
  }
4932
4966
  if (name === "grep") {
4933
4967
  const { keyword, limit, cursor } = args;
4934
- const matches = this.store.searchChapterParagraphs(workId, keyword, limit)
4968
+ const matches = this.store.searchChapterParagraphs(workId, keyword, limit, { excludeAuthorNotes: true })
4935
4969
  .filter((match) => !scopedChapterIds || scopedChapterIds.has(String(match.chapterId)));
4936
4970
  const records = structuralToolResultRecords(matches, maximumRecordChars);
4937
4971
  const result = paginateToolResultRecords(records, cursor, (page, pagination) => ({
@@ -4960,6 +4994,15 @@ export class AiManager {
4960
4994
  : sourceType === "chapter-outline" ? "outline" : sourceType;
4961
4995
  if (!requestedCategories.has(type))
4962
4996
  return [];
4997
+ if (sourceType === "chapter") {
4998
+ try {
4999
+ if (isAuthorNoteChapter(this.store.getChapter(String(item.id))))
5000
+ return [];
5001
+ }
5002
+ catch {
5003
+ return [];
5004
+ }
5005
+ }
4963
5006
  return [{
4964
5007
  ...item,
4965
5008
  ...this.hybridAiSearchDetails(workId, sourceType, String(item.id)),
@@ -7187,7 +7230,14 @@ export class AiManager {
7187
7230
  if (String(chapter.workId) !== workId)
7188
7231
  throw new AppError(400, "CHAPTER_WORK_MISMATCH", "章节不属于当前作品");
7189
7232
  }
7190
- return new Set(chapterIds);
7233
+ return new Set(chapterIds.filter((chapterId) => {
7234
+ try {
7235
+ return !isAuthorNoteChapter(this.store.getChapter(chapterId));
7236
+ }
7237
+ catch {
7238
+ return false;
7239
+ }
7240
+ }));
7191
7241
  }
7192
7242
  if (scope.type === "volume") {
7193
7243
  const volumeIds = [...new Set([
@@ -7215,6 +7265,8 @@ export class AiManager {
7215
7265
  const chapter = this.store.getChapter(sourceId);
7216
7266
  if (String(chapter.workId) !== workId)
7217
7267
  return null;
7268
+ if (isAuthorNoteChapter(chapter))
7269
+ return null;
7218
7270
  return {
7219
7271
  sourceType,
7220
7272
  sourceId,
@@ -8121,8 +8173,7 @@ export class AiManager {
8121
8173
  return null;
8122
8174
  if (scope.type === "volume" && scope.volumeId !== String(chapter.volume_id))
8123
8175
  return null;
8124
- if ((scope.type === "book" || scope.type === "volume")
8125
- && (Boolean(chapter.excluded_from_analysis) || String(chapter.chapter_type) === "作者的话"))
8176
+ if (Boolean(chapter.excluded_from_analysis) || String(chapter.chapter_type) === "作者的话")
8126
8177
  return null;
8127
8178
  return String(chapter.version_no);
8128
8179
  }
@@ -9107,7 +9158,7 @@ export class AiManager {
9107
9158
  });
9108
9159
  }
9109
9160
  isAutomaticAnalysisChapter(chapter) {
9110
- return !chapter.excludedFromAnalysis && chapter.chapterType !== "作者的话";
9161
+ return !chapter.excludedFromAnalysis && !isAuthorNoteChapter(chapter);
9111
9162
  }
9112
9163
  buildChapterChunks(chapters, maximumChars = 10_000) {
9113
9164
  const chunks = [];