@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 +86 -35
- package/dist/ai.js.map +1 -1
- package/dist/public/app.js +206 -80
- package/dist/public/index.html +4 -4
- package/dist/public/stream-typewriter.d.ts +12 -1
- package/dist/public/stream-typewriter.js +65 -5
- package/dist/public/styles.css +73 -17
- package/dist/store.js +187 -31
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
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
|
-
|
|
1388
|
-
|
|
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
|
-
|
|
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.
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
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 ((
|
|
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
|
|
9161
|
+
return !chapter.excludedFromAnalysis && !isAuthorNoteChapter(chapter);
|
|
9111
9162
|
}
|
|
9112
9163
|
buildChapterChunks(chapters, maximumChars = 10_000) {
|
|
9113
9164
|
const chunks = [];
|