@musnows/scriverse 1.1.0 → 1.1.1
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/public/ai-skill-menu.js +25 -5
- package/dist/public/app.js +285 -97
- package/dist/public/chapter-directory.d.ts +22 -0
- package/dist/public/chapter-directory.js +22 -0
- package/dist/public/index.html +2 -2
- package/dist/public/styles.css +6 -3
- package/dist/store.js +15 -8
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyChapterDirectoryMove, chapterDirectoryEntry } from "/chapter-directory.js?v=20260921-directory-performance-v1";
|
|
1
2
|
import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate, normalizeGalaxyMotionMode, renderRelationshipMindMap } from "/relationship-graph.js?v=20260817-relationship-canvas-scale-v1&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1";
|
|
2
3
|
import { formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
|
|
3
4
|
import { countProseWords } from "/text-count.js?v=20260906-chapter-word-count-consistency-v1";
|
|
@@ -6,8 +7,9 @@ import { createStreamingMarkdownRenderer } from "/stream-markdown.js?v=20260912-
|
|
|
6
7
|
import { createAiRenderScheduler } from "/ai-render-scheduler.js?v=20260912-stream-render-v2";
|
|
7
8
|
import { createImWorkspace } from "/im.js?v=20260904-im-judge-outcomes-v106";
|
|
8
9
|
import { findAiMention, listAiMentionOptions, mergeAiReferenceScope } from "/ai-mentions.js?v=20260811-user-message-mentions-v1";
|
|
9
|
-
import { applyAiSkillCommand, findAiSkillCommand,
|
|
10
|
+
import { applyAiSkillCommand, findAiSkillCommand, listAiSlashOptions } from "/ai-skill-menu.js?v=20260921-ai-compact-slash-command-v2";
|
|
10
11
|
import {
|
|
12
|
+
composeRoleplayStoredUserContent,
|
|
11
13
|
emptyRoleplayScenePin,
|
|
12
14
|
normalizeRoleplayScenePin,
|
|
13
15
|
parseRoleplayUserTurn,
|
|
@@ -772,6 +774,12 @@ function cancelActiveAiRequest(reason) {
|
|
|
772
774
|
|
|
773
775
|
function submitAiComposerPrompt() {
|
|
774
776
|
const tab = activeAiChatTab();
|
|
777
|
+
if ($("#ai-task").value === "chat" && aiPromptText().trim() === "/compact") {
|
|
778
|
+
if (aiInteractionBusy()) return toast("当前回复执行中,暂不能压缩上下文", "error");
|
|
779
|
+
hideAiMentionMenu();
|
|
780
|
+
void compactAiConversation({ clearComposer: true });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
775
783
|
if (aiRequestManager.hasActive(tab?.id)) {
|
|
776
784
|
if (aiComposerSendMode() === "steer") {
|
|
777
785
|
void sendActiveComposerAsSteer();
|
|
@@ -5956,13 +5964,13 @@ function updateAiMentionMenu() {
|
|
|
5956
5964
|
aiMentionRange = selection.getRangeAt(0).cloneRange();
|
|
5957
5965
|
const menu = $("#ai-mention-menu");
|
|
5958
5966
|
if (skillMatch) {
|
|
5959
|
-
const options =
|
|
5967
|
+
const options = listAiSlashOptions(skillMatch.query);
|
|
5960
5968
|
aiMentionActiveIndex = -1;
|
|
5961
5969
|
prompt.removeAttribute("aria-activedescendant");
|
|
5962
|
-
menu.setAttribute("aria-label", "
|
|
5970
|
+
menu.setAttribute("aria-label", "选择 Chat Slash 操作");
|
|
5963
5971
|
menu.innerHTML = options.length
|
|
5964
|
-
? options.map((item, index) => `<button id="ai-skill-option-${index}" class="ai-mention-option ai-skill-option" type="button" role="option" aria-selected="false" tabindex="-1" data-ai-skill-name="${esc(item.name)}"><small
|
|
5965
|
-
: '<p class="ai-mention-empty"
|
|
5972
|
+
? options.map((item, index) => `<button id="ai-skill-option-${index}" class="ai-mention-option ai-skill-option" type="button" role="option" aria-selected="false" tabindex="-1" data-ai-command-name="${esc(item.name)}"${item.kind === "skill" ? ` data-ai-skill-name="${esc(item.name)}"` : ""}><small>${item.kind === "skill" ? "Skill" : "压缩"}</small><span><strong>/${esc(item.name)}</strong><em>${esc(item.label)} · ${esc(item.description)}</em></span></button>`).join("")
|
|
5973
|
+
: '<p class="ai-mention-empty">没有匹配的 Chat Slash 操作</p>';
|
|
5966
5974
|
menu.classList.remove("hidden");
|
|
5967
5975
|
prompt.setAttribute("aria-expanded", "true");
|
|
5968
5976
|
return;
|
|
@@ -6040,6 +6048,15 @@ function selectAiSkill(button) {
|
|
|
6040
6048
|
hideAiMentionMenu();
|
|
6041
6049
|
}
|
|
6042
6050
|
|
|
6051
|
+
function selectAiSlashCommand(button) {
|
|
6052
|
+
if (button.dataset.aiCommandName === "compact") {
|
|
6053
|
+
hideAiMentionMenu();
|
|
6054
|
+
void compactAiConversation({ clearComposer: true });
|
|
6055
|
+
return;
|
|
6056
|
+
}
|
|
6057
|
+
selectAiSkill(button);
|
|
6058
|
+
}
|
|
6059
|
+
|
|
6043
6060
|
function addSelectedLinesAsCitation() {
|
|
6044
6061
|
if (!state.chapter || !chapterLineSelection) return;
|
|
6045
6062
|
const selection = selectedChapterLinePayload(chapterLineSelection.start, chapterLineSelection.end);
|
|
@@ -9446,7 +9463,7 @@ async function loadVolumeChapters(volumeId) {
|
|
|
9446
9463
|
const workId = state.work.id;
|
|
9447
9464
|
const generation = workScopedUiGeneration;
|
|
9448
9465
|
volumeChapterLoadingIds.add(volumeId);
|
|
9449
|
-
renderTree();
|
|
9466
|
+
renderTree([volumeId]);
|
|
9450
9467
|
const request = (async () => {
|
|
9451
9468
|
try {
|
|
9452
9469
|
const chapters = await apiAllPages(`/api/volumes/${encodeURIComponent(volumeId)}/chapters`, 100);
|
|
@@ -9456,17 +9473,15 @@ async function loadVolumeChapters(volumeId) {
|
|
|
9456
9473
|
volume.chapters = chapters;
|
|
9457
9474
|
volume.chapterCount = chapters.length;
|
|
9458
9475
|
loadedVolumeChapterIds.add(volumeId);
|
|
9459
|
-
renderTree();
|
|
9460
9476
|
} catch (error) {
|
|
9461
9477
|
if (state.work?.id === workId && generation === workScopedUiGeneration) {
|
|
9462
9478
|
toast(`加载分卷章节失败:${error.message}`, "error");
|
|
9463
|
-
renderTree();
|
|
9464
9479
|
}
|
|
9465
9480
|
} finally {
|
|
9466
9481
|
if (volumeChapterRequests.get(volumeId) === request) {
|
|
9467
9482
|
volumeChapterLoadingIds.delete(volumeId);
|
|
9468
9483
|
volumeChapterRequests.delete(volumeId);
|
|
9469
|
-
if (state.work?.id === workId && generation === workScopedUiGeneration) renderTree();
|
|
9484
|
+
if (state.work?.id === workId && generation === workScopedUiGeneration) renderTree([volumeId]);
|
|
9470
9485
|
}
|
|
9471
9486
|
}
|
|
9472
9487
|
})();
|
|
@@ -9490,20 +9505,7 @@ function mergeChapterDirectoryEntry(chapter) {
|
|
|
9490
9505
|
if (!state.work || !chapter?.volumeId) return;
|
|
9491
9506
|
const volume = state.work.volumes.find((item) => item.id === chapter.volumeId);
|
|
9492
9507
|
if (!volume) return;
|
|
9493
|
-
const directoryEntry =
|
|
9494
|
-
id: chapter.id,
|
|
9495
|
-
workId: chapter.workId,
|
|
9496
|
-
volumeId: chapter.volumeId,
|
|
9497
|
-
title: chapter.title,
|
|
9498
|
-
chapterType: chapter.chapterType,
|
|
9499
|
-
sortOrder: chapter.sortOrder,
|
|
9500
|
-
wordCount: chapter.wordCount,
|
|
9501
|
-
versionNo: chapter.versionNo,
|
|
9502
|
-
analysisStatus: chapter.analysisStatus,
|
|
9503
|
-
excludedFromAnalysis: chapter.excludedFromAnalysis,
|
|
9504
|
-
createdAt: chapter.createdAt,
|
|
9505
|
-
updatedAt: chapter.updatedAt
|
|
9506
|
-
};
|
|
9508
|
+
const directoryEntry = chapterDirectoryEntry(chapter);
|
|
9507
9509
|
const chapters = Array.isArray(volume.chapters) ? volume.chapters : [];
|
|
9508
9510
|
const existingIndex = chapters.findIndex((item) => item.id === chapter.id);
|
|
9509
9511
|
if (existingIndex >= 0) chapters[existingIndex] = directoryEntry;
|
|
@@ -9513,14 +9515,61 @@ function mergeChapterDirectoryEntry(chapter) {
|
|
|
9513
9515
|
volume.chapterCount = Math.max(Number(volume.chapterCount ?? 0), chapters.length);
|
|
9514
9516
|
}
|
|
9515
9517
|
|
|
9516
|
-
function
|
|
9518
|
+
function syncChapterTreeSelection() {
|
|
9519
|
+
const tree = $("#novel-tree");
|
|
9520
|
+
tree.querySelectorAll(".chapter-node.active").forEach((node) => node.classList.remove("active"));
|
|
9521
|
+
const chapter = state.chapter;
|
|
9522
|
+
if (!chapter) return;
|
|
9523
|
+
let button = tree.querySelector(`[data-chapter-id="${CSS.escape(chapter.id)}"]`);
|
|
9524
|
+
if (!button || button.closest(".volume-node")?.classList.contains("is-collapsed")) {
|
|
9525
|
+
renderTree([chapter.volumeId]);
|
|
9526
|
+
button = tree.querySelector(`[data-chapter-id="${CSS.escape(chapter.id)}"]`);
|
|
9527
|
+
}
|
|
9528
|
+
if (!button) return;
|
|
9529
|
+
button.classList.add("active");
|
|
9530
|
+
button.firstElementChild.textContent = chapter.title;
|
|
9531
|
+
button.querySelector("small").textContent = Number(chapter.wordCount ?? 0).toLocaleString("zh-CN");
|
|
9532
|
+
const badge = button.querySelector(".chapter-type-badge")?.textContent ?? "";
|
|
9533
|
+
const chapterType = chapter.chapterType && chapter.chapterType !== "正文" ? chapter.chapterType : "";
|
|
9534
|
+
if (badge !== chapterType) {
|
|
9535
|
+
button.querySelector(".chapter-node-meta").innerHTML = `${chapterType ? `<em class="chapter-type-badge">${esc(chapterType)}</em>` : ""}<small>${Number(chapter.wordCount ?? 0).toLocaleString("zh-CN")}</small>`;
|
|
9536
|
+
}
|
|
9537
|
+
}
|
|
9538
|
+
|
|
9539
|
+
function syncMovedChapterTree(chapterId, volumeIds) {
|
|
9540
|
+
const tree = $("#novel-tree");
|
|
9541
|
+
const location = findChapterLocation(chapterId);
|
|
9542
|
+
const button = tree.querySelector(`[data-chapter-id="${CSS.escape(chapterId)}"]`);
|
|
9543
|
+
const target = location && tree.querySelector(`[data-volume-id="${CSS.escape(location.volume.id)}"] .volume-chapters`);
|
|
9544
|
+
if (!button || !target) return renderTree(volumeIds);
|
|
9545
|
+
if (state.collapsedVolumeIds.has(location.volume.id)) button.remove();
|
|
9546
|
+
else {
|
|
9547
|
+
target.querySelector(".entity-history-empty")?.remove();
|
|
9548
|
+
const nextChapter = location.volume.chapters[location.chapterIndex + 1];
|
|
9549
|
+
const nextButton = nextChapter ? target.querySelector(`[data-chapter-id="${CSS.escape(nextChapter.id)}"]`) : null;
|
|
9550
|
+
target.insertBefore(button, nextButton);
|
|
9551
|
+
}
|
|
9552
|
+
for (const volumeId of volumeIds) {
|
|
9553
|
+
const volume = state.work.volumes.find((item) => item.id === volumeId);
|
|
9554
|
+
const node = tree.querySelector(`[data-volume-id="${CSS.escape(volumeId)}"]`);
|
|
9555
|
+
if (!volume || !node) continue;
|
|
9556
|
+
node.querySelector(".volume-chapter-count-number").textContent = String(volume.chapterCount);
|
|
9557
|
+
if (!volume.chapters.length && !state.collapsedVolumeIds.has(volumeId)) {
|
|
9558
|
+
node.querySelector(".volume-chapters").innerHTML = '<p class="entity-history-empty">本卷还没有章节。</p>';
|
|
9559
|
+
}
|
|
9560
|
+
}
|
|
9561
|
+
}
|
|
9562
|
+
|
|
9563
|
+
function renderTree(volumeIds = null) {
|
|
9517
9564
|
if (!state.work) return;
|
|
9518
9565
|
const count = state.work.volumes.reduce((total, volume) => total + Number(volume.chapterCount ?? volume.chapters?.length ?? 0), 0);
|
|
9519
9566
|
const proseEditable = canEditProse();
|
|
9520
9567
|
$("#chapter-count").querySelector(".chapter-count-number").textContent = String(count);
|
|
9521
9568
|
$("#reader-open-button").disabled = !canReadModule("editor") || count === 0;
|
|
9522
9569
|
$("#novel-tree").classList.remove("empty-copy");
|
|
9523
|
-
$("#novel-tree")
|
|
9570
|
+
const tree = $("#novel-tree");
|
|
9571
|
+
const volumes = volumeIds ? state.work.volumes.filter((volume) => volumeIds.includes(volume.id)) : state.work.volumes;
|
|
9572
|
+
const markup = volumes.map((volume) => {
|
|
9524
9573
|
const collapsed = state.collapsedVolumeIds.has(volume.id);
|
|
9525
9574
|
const chapters = Array.isArray(volume.chapters) ? volume.chapters : [];
|
|
9526
9575
|
const chapterContent = collapsed
|
|
@@ -9531,8 +9580,9 @@ function renderTree() {
|
|
|
9531
9580
|
? '<p class="entity-history-empty">展开后加载章节。</p>'
|
|
9532
9581
|
: chapters.length
|
|
9533
9582
|
? chapters.map((chapter) => `
|
|
9534
|
-
<button class="chapter-node ${state.chapter?.id === chapter.id ? "active" : ""}" type="button" data-chapter-id="${esc(chapter.id)}" draggable="
|
|
9583
|
+
<button class="chapter-node ${state.chapter?.id === chapter.id ? "active" : ""}" type="button" data-chapter-id="${esc(chapter.id)}" draggable="false" title="点击打开章节${proseEditable ? ";拖动左侧手柄排序;Alt+方向键排序,Alt+Shift+方向键跨卷" : ""}">
|
|
9535
9584
|
<span>${esc(chapter.title)}</span><span class="chapter-node-meta">${chapter.chapterType && chapter.chapterType !== "正文" ? `<em class="chapter-type-badge">${esc(chapter.chapterType)}</em>` : ""}<small>${Number(chapter.wordCount ?? 0).toLocaleString("zh-CN")}</small></span>
|
|
9585
|
+
${proseEditable ? '<span class="chapter-drag-handle" draggable="true" aria-hidden="true" title="拖动排序"><svg viewBox="0 0 12 18" focusable="false"><path d="M4 4h.01M8 4h.01M4 9h.01M8 9h.01M4 14h.01M8 14h.01"></path></svg></span>' : ""}
|
|
9536
9586
|
</button>`).join("")
|
|
9537
9587
|
: '<p class="entity-history-empty">本卷还没有章节。</p>';
|
|
9538
9588
|
return `
|
|
@@ -9546,15 +9596,28 @@ function renderTree() {
|
|
|
9546
9596
|
${chapterContent}
|
|
9547
9597
|
</div>
|
|
9548
9598
|
</div>`;
|
|
9549
|
-
})
|
|
9550
|
-
|
|
9599
|
+
});
|
|
9600
|
+
const roots = [];
|
|
9601
|
+
if (volumeIds) {
|
|
9602
|
+
volumes.forEach((volume, index) => {
|
|
9603
|
+
const node = tree.querySelector(`[data-volume-id="${CSS.escape(volume.id)}"]`);
|
|
9604
|
+
if (!node) return;
|
|
9605
|
+
node.outerHTML = markup[index];
|
|
9606
|
+
roots.push(tree.querySelector(`[data-volume-id="${CSS.escape(volume.id)}"]`));
|
|
9607
|
+
});
|
|
9608
|
+
} else {
|
|
9609
|
+
tree.innerHTML = markup.join("");
|
|
9610
|
+
roots.push(tree);
|
|
9611
|
+
}
|
|
9612
|
+
const renderedNodes = (selector) => roots.flatMap((root) => Array.from(root.querySelectorAll(selector)));
|
|
9613
|
+
renderedNodes("[data-volume-toggle]").forEach((button) => {
|
|
9551
9614
|
button.addEventListener("click", () => {
|
|
9552
9615
|
const volumeId = button.dataset.volumeToggle;
|
|
9553
9616
|
if (state.collapsedVolumeIds.has(volumeId)) {
|
|
9554
9617
|
state.collapsedVolumeIds.delete(volumeId);
|
|
9555
9618
|
void loadVolumeChapters(volumeId);
|
|
9556
9619
|
} else state.collapsedVolumeIds.add(volumeId);
|
|
9557
|
-
renderTree();
|
|
9620
|
+
renderTree([volumeId]);
|
|
9558
9621
|
});
|
|
9559
9622
|
button.addEventListener("contextmenu", (event) => {
|
|
9560
9623
|
if (!canEditProse()) return;
|
|
@@ -9582,18 +9645,19 @@ function renderTree() {
|
|
|
9582
9645
|
});
|
|
9583
9646
|
}
|
|
9584
9647
|
});
|
|
9585
|
-
|
|
9648
|
+
renderedNodes("[data-volume-detail]").forEach((button) => {
|
|
9586
9649
|
button.addEventListener("click", () => {
|
|
9587
9650
|
openVolumeDialog(state.work.volumes.find((volume) => volume.id === button.dataset.volumeDetail));
|
|
9588
9651
|
});
|
|
9589
9652
|
});
|
|
9590
|
-
|
|
9653
|
+
renderedNodes("[data-new-chapter-volume]").forEach((button) => {
|
|
9591
9654
|
button.addEventListener("click", () => openChapterDialog(button.dataset.newChapterVolume));
|
|
9592
9655
|
});
|
|
9593
|
-
|
|
9594
|
-
button.addEventListener("click", async () => {
|
|
9656
|
+
renderedNodes("[data-chapter-id]").forEach((button) => {
|
|
9657
|
+
button.addEventListener("click", async (event) => {
|
|
9658
|
+
if (event.target.closest(".chapter-drag-handle")) return;
|
|
9595
9659
|
const chapterId = button.dataset.chapterId;
|
|
9596
|
-
await selectChapter(chapterId);
|
|
9660
|
+
if (!(await selectChapter(chapterId))) return;
|
|
9597
9661
|
$("#novel-tree").querySelector(`[data-chapter-id="${CSS.escape(chapterId)}"]`)?.focus();
|
|
9598
9662
|
if (isMobileViewport()) {
|
|
9599
9663
|
panelLayout.leftCollapsed = true;
|
|
@@ -9614,6 +9678,10 @@ function renderTree() {
|
|
|
9614
9678
|
});
|
|
9615
9679
|
if (proseEditable) {
|
|
9616
9680
|
button.addEventListener("dragstart", (event) => {
|
|
9681
|
+
if (!canEditProse() || !event.target.closest(".chapter-drag-handle")) {
|
|
9682
|
+
event.preventDefault();
|
|
9683
|
+
return;
|
|
9684
|
+
}
|
|
9617
9685
|
event.dataTransfer?.setData("text/plain", button.dataset.chapterId);
|
|
9618
9686
|
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
|
9619
9687
|
button.classList.add("is-dragging");
|
|
@@ -9636,7 +9704,7 @@ function renderTree() {
|
|
|
9636
9704
|
const target = findChapterLocation(button.dataset.chapterId);
|
|
9637
9705
|
const after = button.classList.contains("drop-after");
|
|
9638
9706
|
button.classList.remove("is-drag-over", "drop-after");
|
|
9639
|
-
if (!chapterId || !target) return;
|
|
9707
|
+
if (!chapterId || !target || chapterId === button.dataset.chapterId) return;
|
|
9640
9708
|
const targetChapters = target.volume.chapters.filter((chapter) => chapter.id !== chapterId);
|
|
9641
9709
|
const targetIndex = targetChapters.findIndex((chapter) => chapter.id === button.dataset.chapterId);
|
|
9642
9710
|
await moveChapterInTree(chapterId, target.volume.id, Math.max(0, targetIndex + (after ? 1 : 0)));
|
|
@@ -9789,22 +9857,36 @@ function findChapterLocation(chapterId) {
|
|
|
9789
9857
|
|
|
9790
9858
|
async function moveChapterInTree(chapterId, volumeId, sortOrder) {
|
|
9791
9859
|
const location = findChapterLocation(chapterId);
|
|
9792
|
-
if (!location || chapterMovePending) return;
|
|
9860
|
+
if (!location || chapterMovePending || !canEditProse()) return;
|
|
9793
9861
|
const samePosition = location.volume.id === volumeId && location.chapterIndex === sortOrder;
|
|
9794
9862
|
if (samePosition) return;
|
|
9863
|
+
const workId = state.work.id;
|
|
9864
|
+
const generation = workScopedUiGeneration;
|
|
9865
|
+
const volumeIds = [...new Set([location.volume.id, volumeId])];
|
|
9795
9866
|
chapterMovePending = true;
|
|
9796
9867
|
try {
|
|
9868
|
+
await Promise.all(volumeIds.map((id) => loadVolumeChapters(id)));
|
|
9869
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
9870
|
+
if (volumeIds.some((id) => !loadedVolumeChapterIds.has(id))) return;
|
|
9797
9871
|
const moved = await api(`/api/chapters/${encodeURIComponent(chapterId)}/move`, {
|
|
9798
9872
|
method: "POST",
|
|
9799
9873
|
body: { volumeId, sortOrder, expectedVersionNo: location.chapter.versionNo }
|
|
9800
9874
|
});
|
|
9801
|
-
state.work
|
|
9802
|
-
|
|
9803
|
-
|
|
9804
|
-
|
|
9875
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
9876
|
+
const affectedVolumeIds = applyChapterDirectoryMove(state.work, moved);
|
|
9877
|
+
const selectedEntry = state.chapter && findChapterLocation(state.chapter.id)?.chapter;
|
|
9878
|
+
if (selectedEntry) {
|
|
9879
|
+
state.chapter = { ...state.chapter, ...selectedEntry };
|
|
9880
|
+
updateChapterPath();
|
|
9881
|
+
updateChapterStats();
|
|
9882
|
+
}
|
|
9883
|
+
syncMovedChapterTree(chapterId, affectedVolumeIds);
|
|
9884
|
+
const focusTarget = $("#novel-tree").querySelector(`[data-chapter-id="${CSS.escape(chapterId)}"]`)
|
|
9885
|
+
?? $("#novel-tree").querySelector(`[data-volume-toggle="${CSS.escape(volumeId)}"]`);
|
|
9886
|
+
focusTarget?.focus();
|
|
9805
9887
|
toast(location.volume.id === volumeId ? "章节顺序已更新" : "章节已移动到目标分卷");
|
|
9806
9888
|
} catch (error) {
|
|
9807
|
-
toast(error.message, "error");
|
|
9889
|
+
if (state.work?.id === workId && generation === workScopedUiGeneration) toast(error.message, "error");
|
|
9808
9890
|
} finally {
|
|
9809
9891
|
chapterMovePending = false;
|
|
9810
9892
|
}
|
|
@@ -9816,7 +9898,7 @@ async function moveChapterByKeyboard(chapterId, direction, crossVolume) {
|
|
|
9816
9898
|
if (crossVolume) {
|
|
9817
9899
|
const targetVolume = state.work.volumes[location.volumeIndex + direction];
|
|
9818
9900
|
if (!targetVolume) return toast("已经是最前或最后一个分卷");
|
|
9819
|
-
await moveChapterInTree(chapterId, targetVolume.id, direction < 0 ? targetVolume.chapters.length : 0);
|
|
9901
|
+
await moveChapterInTree(chapterId, targetVolume.id, direction < 0 ? Number(targetVolume.chapterCount ?? targetVolume.chapters.length) : 0);
|
|
9820
9902
|
return;
|
|
9821
9903
|
}
|
|
9822
9904
|
const targetIndex = location.chapterIndex + direction;
|
|
@@ -10131,10 +10213,7 @@ async function selectChapter(chapterId, { editMode = false } = {}) {
|
|
|
10131
10213
|
$("#welcome-view").classList.add("hidden");
|
|
10132
10214
|
$("#module-view").classList.add("hidden");
|
|
10133
10215
|
$("#editor-view").classList.remove("hidden");
|
|
10134
|
-
|
|
10135
|
-
const chapterPath = `${volume?.title ?? "正文"} / 保存于 ${formatDateTime(state.chapter.updatedAt)}`;
|
|
10136
|
-
$("#chapter-path").textContent = chapterPath;
|
|
10137
|
-
$("#chapter-path").title = chapterPath;
|
|
10216
|
+
updateChapterPath();
|
|
10138
10217
|
$("#chapter-title").value = state.chapter.title;
|
|
10139
10218
|
$("#chapter-content").value = state.chapter.content;
|
|
10140
10219
|
resetChapterDraftLineIds(state.chapter);
|
|
@@ -10149,12 +10228,20 @@ async function selectChapter(chapterId, { editMode = false } = {}) {
|
|
|
10149
10228
|
if (!canEditProse()) setSaveState("正文只读");
|
|
10150
10229
|
else if (chapterEditorReadOnly) setSaveState("阅读模式");
|
|
10151
10230
|
else setSaveState("已保存");
|
|
10152
|
-
|
|
10231
|
+
syncChapterTreeSelection();
|
|
10153
10232
|
replacePageRoute({ view: "editor", workId: state.work.id, chapterId: state.chapter.id });
|
|
10154
|
-
|
|
10233
|
+
void loadChapterForeshadowReminders();
|
|
10155
10234
|
return true;
|
|
10156
10235
|
}
|
|
10157
10236
|
|
|
10237
|
+
function updateChapterPath() {
|
|
10238
|
+
if (!state.work || !state.chapter) return;
|
|
10239
|
+
const volume = state.work.volumes.find((item) => item.id === state.chapter.volumeId);
|
|
10240
|
+
const chapterPath = `${volume?.title ?? "正文"} / 保存于 ${formatDateTime(state.chapter.updatedAt)}`;
|
|
10241
|
+
$("#chapter-path").textContent = chapterPath;
|
|
10242
|
+
$("#chapter-path").title = chapterPath;
|
|
10243
|
+
}
|
|
10244
|
+
|
|
10158
10245
|
function updateChapterStats() {
|
|
10159
10246
|
if (!state.chapter) return;
|
|
10160
10247
|
const text = $("#chapter-content").value;
|
|
@@ -15486,6 +15573,43 @@ function setAiContextWarningActionsDisabled(disabled) {
|
|
|
15486
15573
|
for (const button of $("#ai-context-warning").querySelectorAll("button")) button.disabled = disabled;
|
|
15487
15574
|
}
|
|
15488
15575
|
|
|
15576
|
+
async function compactAiConversation({ clearComposer = false, button = null } = {}) {
|
|
15577
|
+
if (aiInteractionBusy()) return toast("当前回复执行中,暂不能压缩上下文", "error");
|
|
15578
|
+
const requestScope = currentAiRequestScope();
|
|
15579
|
+
const modelId = $("#ai-model").value;
|
|
15580
|
+
if (!requestScope || !modelId) return toast("请先选择章节和模型", "error");
|
|
15581
|
+
const previousLabel = button?.textContent ?? "";
|
|
15582
|
+
setAiContextWarningActionsDisabled(true);
|
|
15583
|
+
if (button) button.textContent = "压缩中";
|
|
15584
|
+
try {
|
|
15585
|
+
const conversationId = await ensureAiConversation();
|
|
15586
|
+
const result = await api(`/api/ai-conversations/${conversationId}/compact`, {
|
|
15587
|
+
method: "POST",
|
|
15588
|
+
body: { modelId, scope: requestScope.scope }
|
|
15589
|
+
});
|
|
15590
|
+
hideAiContextWarning();
|
|
15591
|
+
if (clearComposer) clearAiPromptComposer();
|
|
15592
|
+
$("#ai-prompt").focus();
|
|
15593
|
+
toast(result.changed ? `已整理 ${result.compactedMessageCount} 条较早消息为长期记忆` : "当前没有需要整理的较早消息");
|
|
15594
|
+
setAiContextMeter(result.usage);
|
|
15595
|
+
if (result.changed) {
|
|
15596
|
+
const current = state.aiConversations.find((conversation) => conversation.id === conversationId);
|
|
15597
|
+
if (current) {
|
|
15598
|
+
current.compactedMessageCount = result.compactedMessageCount;
|
|
15599
|
+
current.hasCompactedSummary = true;
|
|
15600
|
+
}
|
|
15601
|
+
renderConversationCompactionDivider(result.compactedMessageCount, current?.messageCount);
|
|
15602
|
+
}
|
|
15603
|
+
return result;
|
|
15604
|
+
} catch (error) {
|
|
15605
|
+
toast(`上下文压缩失败:${error.message}`, "error");
|
|
15606
|
+
return null;
|
|
15607
|
+
} finally {
|
|
15608
|
+
setAiContextWarningActionsDisabled(false);
|
|
15609
|
+
if (button) button.textContent = previousLabel;
|
|
15610
|
+
}
|
|
15611
|
+
}
|
|
15612
|
+
|
|
15489
15613
|
async function loadAiReferences() {
|
|
15490
15614
|
const workId = state.work?.id;
|
|
15491
15615
|
if (!workId) return;
|
|
@@ -18945,6 +19069,23 @@ async function sendAi() {
|
|
|
18945
19069
|
return sendAiWithOptions();
|
|
18946
19070
|
}
|
|
18947
19071
|
|
|
19072
|
+
function appendAiPendingRequestMessage(tab) {
|
|
19073
|
+
const message = document.createElement("div");
|
|
19074
|
+
message.className = "assistant-message is-streaming";
|
|
19075
|
+
message.dataset.testid = "ai-stream-message";
|
|
19076
|
+
message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite" aria-busy="true"></div><div class="message-meta">正在请求……</div>';
|
|
19077
|
+
attachMessageHeading(message, aiAssistantLabel("正在请求", tab.roleplayCharacter), undefined, tab);
|
|
19078
|
+
tab.feed.append(message);
|
|
19079
|
+
scrollAiFeedToBottom(tab.feed);
|
|
19080
|
+
return message;
|
|
19081
|
+
}
|
|
19082
|
+
|
|
19083
|
+
function prefixAiRequestError(error, prefix) {
|
|
19084
|
+
const failure = error instanceof Error ? error : new Error(String(error ?? "未知错误"));
|
|
19085
|
+
failure.message = `${prefix}:${failure.message}`;
|
|
19086
|
+
return failure;
|
|
19087
|
+
}
|
|
19088
|
+
|
|
18948
19089
|
async function sendAiWithOptions({ ignoreContextWarning = false, retry = null } = {}) {
|
|
18949
19090
|
if (!state.work) return toast("请先选择作品", "error");
|
|
18950
19091
|
const tab = activeAiChatTab();
|
|
@@ -18994,7 +19135,10 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
18994
19135
|
workId: state.work.id,
|
|
18995
19136
|
conversationId: state.aiConversationId
|
|
18996
19137
|
}),
|
|
18997
|
-
preserveComposer: Boolean(queuedComposer)
|
|
19138
|
+
preserveComposer: Boolean(queuedComposer),
|
|
19139
|
+
optimisticUserMessage: null,
|
|
19140
|
+
pendingAssistantMessage: null,
|
|
19141
|
+
requestStartedAt: Date.now()
|
|
18998
19142
|
};
|
|
18999
19143
|
if (retry?.userMessageId) {
|
|
19000
19144
|
requestHolder.snapshot = aiRequestManager.bind(requestHolder.snapshot, { userMessageId: retry.userMessageId });
|
|
@@ -19002,6 +19146,33 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
19002
19146
|
setAiChatTabStatus(tab, "streaming");
|
|
19003
19147
|
syncAiRequestControls();
|
|
19004
19148
|
scrollAiFeedToBottom(tab.feed, { force: true });
|
|
19149
|
+
if (!retry) {
|
|
19150
|
+
const optimisticContent = selectedTaskType === "roleplay"
|
|
19151
|
+
? composeRoleplayStoredUserContent(sceneDirection, instruction)
|
|
19152
|
+
: instruction;
|
|
19153
|
+
requestHolder.optimisticUserMessage = appendMessage(
|
|
19154
|
+
"user",
|
|
19155
|
+
optimisticContent,
|
|
19156
|
+
citations,
|
|
19157
|
+
null,
|
|
19158
|
+
{
|
|
19159
|
+
...(requestScope.scope.characterIds?.length ? { mentionCharacterIds: requestScope.scope.characterIds } : {}),
|
|
19160
|
+
...(requestScope.scope.settingIds?.length ? { mentionSettingIds: requestScope.scope.settingIds } : {}),
|
|
19161
|
+
...(requestScope.scope.chapterIds?.length ? { mentionChapterIds: requestScope.scope.chapterIds } : {}),
|
|
19162
|
+
...(requestScope.scope.includeSettingInfo === true ? { mentionContextSettingIds: ["include-setting-info"] } : {}),
|
|
19163
|
+
...(requestComposerSnapshot.images.length ? { chatImageAttachmentIds: aiChatImageAttachmentIds(requestComposerSnapshot.images) } : {})
|
|
19164
|
+
},
|
|
19165
|
+
null,
|
|
19166
|
+
{ tab }
|
|
19167
|
+
);
|
|
19168
|
+
requestHolder.optimisticUserMessage.dataset.status = "pending";
|
|
19169
|
+
requestHolder.optimisticUserMessage.setAttribute("aria-label", "消息正在发送");
|
|
19170
|
+
if (!requestHolder.preserveComposer) {
|
|
19171
|
+
clearAiChatTabComposer(tab);
|
|
19172
|
+
if (isActiveAiChatTab(tab)) clearAiPromptComposer({ collapseScenePanel: Boolean(sceneDirection) });
|
|
19173
|
+
}
|
|
19174
|
+
requestHolder.pendingAssistantMessage = appendAiPendingRequestMessage(tab);
|
|
19175
|
+
}
|
|
19005
19176
|
let shouldFlushQueuedPrompt = false;
|
|
19006
19177
|
try {
|
|
19007
19178
|
try {
|
|
@@ -19010,21 +19181,31 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
19010
19181
|
} catch (error) {
|
|
19011
19182
|
if (isAiRequestCancellation(error, requestHolder.snapshot) || !aiRequestTargetsCurrentState(requestHolder.snapshot)) throw error;
|
|
19012
19183
|
setAiChatTabStatus(tab, "error");
|
|
19013
|
-
|
|
19184
|
+
const failure = prefixAiRequestError(error, "创作助手加载失败");
|
|
19185
|
+
if (!requestHolder.optimisticUserMessage) return toast(failure.message, "error");
|
|
19186
|
+
throw failure;
|
|
19014
19187
|
}
|
|
19015
19188
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
19016
19189
|
const modelId = tab.selectedModelId || state.models[0]?.id || (isActiveAiChatTab(tab) ? $("#ai-model").value : "");
|
|
19017
|
-
if (!modelId)
|
|
19190
|
+
if (!modelId) {
|
|
19191
|
+
const error = new Error("请先在 AI 管理中配置并选择模型");
|
|
19192
|
+
if (!requestHolder.optimisticUserMessage) return toast(error.message, "error");
|
|
19193
|
+
throw error;
|
|
19194
|
+
}
|
|
19018
19195
|
tab.selectedModelId = modelId;
|
|
19019
19196
|
const imageAttachmentIds = aiChatImageAttachmentIds(requestComposerSnapshot.images);
|
|
19020
19197
|
if (imageAttachmentIds.length > 0 && !state.models.find((model) => model.id === modelId)?.multimodalEnabled) {
|
|
19021
|
-
|
|
19198
|
+
const error = new Error("当前选择的模型不是多模态模型,无法发送图片附件");
|
|
19199
|
+
if (!requestHolder.optimisticUserMessage) return toast(error.message, "error");
|
|
19200
|
+
throw error;
|
|
19022
19201
|
}
|
|
19023
19202
|
try {
|
|
19024
19203
|
await prepareAiRequestConversation(requestHolder, selectedTaskType, requestScope.conversationScope);
|
|
19025
19204
|
} catch (error) {
|
|
19026
19205
|
if (isAiRequestCancellation(error, requestHolder.snapshot) || !aiRequestTargetsCurrentState(requestHolder.snapshot)) throw error;
|
|
19027
|
-
|
|
19206
|
+
const failure = prefixAiRequestError(error, "对话配置锁定失败");
|
|
19207
|
+
if (!requestHolder.optimisticUserMessage) return toast(failure.message, "error");
|
|
19208
|
+
throw failure;
|
|
19028
19209
|
}
|
|
19029
19210
|
setAiChatTabStatus(tab, "streaming");
|
|
19030
19211
|
if (retry?.message?.isConnected) retry.message.remove();
|
|
@@ -19045,7 +19226,12 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
19045
19226
|
...(ignoreContextWarning ? { ignoreContextWarning: true } : {})
|
|
19046
19227
|
}, retry), createAiIdempotencyKey());
|
|
19047
19228
|
const streamedRequest = assertAiRequestCurrent(requestHolder.snapshot);
|
|
19048
|
-
if (streamed.action === "warn")
|
|
19229
|
+
if (streamed.action === "warn") {
|
|
19230
|
+
requestHolder.optimisticUserMessage?.remove();
|
|
19231
|
+
setAiChatTabComposerSnapshot(tab, requestComposerSnapshot);
|
|
19232
|
+
if (isActiveAiChatTab(tab) && !requestHolder.preserveComposer) restoreAiPromptComposer(requestComposerSnapshot);
|
|
19233
|
+
return;
|
|
19234
|
+
}
|
|
19049
19235
|
assistantContent = streamed.content;
|
|
19050
19236
|
shouldFlushQueuedPrompt = true;
|
|
19051
19237
|
assistantMessage = streamed.message;
|
|
@@ -19086,6 +19272,7 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
19086
19272
|
}
|
|
19087
19273
|
} catch (error) {
|
|
19088
19274
|
const request = requestHolder.snapshot;
|
|
19275
|
+
if (!error?.streamInterruption) requestHolder.pendingAssistantMessage?.remove();
|
|
19089
19276
|
if (isAiRequestCancellation(error, request) || !aiRequestTargetsCurrentState(request)) {
|
|
19090
19277
|
const persistedInterruption = await persistAiRequestInterruption(request, error?.streamInterruption);
|
|
19091
19278
|
const userStoppedCurrentReply = request.signal.reason instanceof Error
|
|
@@ -19104,6 +19291,7 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
19104
19291
|
return;
|
|
19105
19292
|
}
|
|
19106
19293
|
if (error?.code === "AI_CONVERSATION_RESPONSE_IN_PROGRESS") {
|
|
19294
|
+
requestHolder.optimisticUserMessage?.remove();
|
|
19107
19295
|
setAiChatTabComposerSnapshot(tab, requestComposerSnapshot);
|
|
19108
19296
|
if (isActiveAiChatTab(tab)) {
|
|
19109
19297
|
if (retry) restoreAiPromptComposer(requestComposerSnapshot);
|
|
@@ -19123,6 +19311,10 @@ async function sendAiWithOptions({ ignoreContextWarning = false, retry = null }
|
|
|
19123
19311
|
return;
|
|
19124
19312
|
}
|
|
19125
19313
|
setAiChatTabStatus(tab, "error");
|
|
19314
|
+
if (requestHolder.optimisticUserMessage?.dataset.status === "pending") {
|
|
19315
|
+
requestHolder.optimisticUserMessage.dataset.status = "failed";
|
|
19316
|
+
requestHolder.optimisticUserMessage.setAttribute("aria-label", "消息发送失败");
|
|
19317
|
+
}
|
|
19126
19318
|
const interruption = error?.streamInterruption;
|
|
19127
19319
|
if (interruption?.content) {
|
|
19128
19320
|
try {
|
|
@@ -19209,14 +19401,21 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
19209
19401
|
const tab = aiChatTabForRequest(requestHolder.snapshot);
|
|
19210
19402
|
if (!tab) throw createAiRequestAbortError("Agent 对话页签已关闭");
|
|
19211
19403
|
const feed = tab.feed;
|
|
19212
|
-
let message =
|
|
19404
|
+
let message = requestHolder.pendingAssistantMessage?.isConnected
|
|
19405
|
+
? requestHolder.pendingAssistantMessage
|
|
19406
|
+
: document.createElement("div");
|
|
19213
19407
|
message.className = "assistant-message is-streaming";
|
|
19214
19408
|
message.dataset.testid = "ai-stream-message";
|
|
19215
|
-
const streamConnectionStartedAt = Date.now();
|
|
19216
|
-
message.
|
|
19409
|
+
const streamConnectionStartedAt = requestHolder.requestStartedAt ?? Date.now();
|
|
19410
|
+
if (!message.querySelector(".message-body")) {
|
|
19411
|
+
message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite" aria-busy="true"></div><div class="message-meta"></div>';
|
|
19412
|
+
}
|
|
19217
19413
|
let content = message.querySelector(".message-body");
|
|
19218
19414
|
let meta = message.querySelector(".message-meta");
|
|
19219
|
-
const connectionSeconds =
|
|
19415
|
+
const connectionSeconds = document.createElement("span");
|
|
19416
|
+
connectionSeconds.className = "ai-stream-connection-seconds";
|
|
19417
|
+
connectionSeconds.dataset.testid = "ai-stream-connection-seconds";
|
|
19418
|
+
meta.replaceChildren("正在请求…… ", connectionSeconds, " 秒");
|
|
19220
19419
|
const renderStreamConnectionElapsed = () => {
|
|
19221
19420
|
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - streamConnectionStartedAt) / 1000));
|
|
19222
19421
|
connectionSeconds.textContent = String(elapsedSeconds);
|
|
@@ -19230,11 +19429,12 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
19230
19429
|
};
|
|
19231
19430
|
const streamConnectionEstablishedEvents = new Set(["continuation", "delta", "process_step", "tool_call", "context_compacted", "complete", "request_status", "error"]);
|
|
19232
19431
|
const streamSpeedController = createStreamTypewriterSpeedController();
|
|
19233
|
-
let
|
|
19432
|
+
let streamConnectionEstablished = false;
|
|
19433
|
+
let messageMounted = message.isConnected;
|
|
19234
19434
|
const mountAssistantMessage = () => {
|
|
19235
19435
|
if (messageMounted) return true;
|
|
19236
19436
|
if (!aiRequestTargetsCurrentState(requestHolder.snapshot)) return false;
|
|
19237
|
-
attachMessageHeading(message, aiAssistantLabel("正在生成", tab.roleplayCharacter), undefined, tab);
|
|
19437
|
+
attachMessageHeading(message, aiAssistantLabel(streamConnectionEstablished ? "正在生成" : "正在请求", tab.roleplayCharacter), undefined, tab);
|
|
19238
19438
|
feed.append(message);
|
|
19239
19439
|
messageMounted = true;
|
|
19240
19440
|
scrollAiFeedToBottom(feed);
|
|
@@ -19356,7 +19556,14 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
19356
19556
|
let streamError = null;
|
|
19357
19557
|
const consume = async (eventName, payload) => {
|
|
19358
19558
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
19359
|
-
if (streamConnectionEstablishedEvents.has(eventName) || eventName === "steer")
|
|
19559
|
+
if (streamConnectionEstablishedEvents.has(eventName) || eventName === "steer") {
|
|
19560
|
+
stopStreamConnectionTimer();
|
|
19561
|
+
streamConnectionEstablished = true;
|
|
19562
|
+
const headingRole = messageMounted ? message.querySelector(".message-heading > span") : null;
|
|
19563
|
+
if (headingRole && !["complete", "request_status", "error"].includes(eventName)) {
|
|
19564
|
+
headingRole.textContent = aiAssistantLabel("正在生成", tab.roleplayCharacter);
|
|
19565
|
+
}
|
|
19566
|
+
}
|
|
19360
19567
|
if (eventName === "continuation") {
|
|
19361
19568
|
if (String(payload.conversationId ?? "") !== requestHolder.snapshot.conversationId) throw new Error("流式续接返回了其他对话");
|
|
19362
19569
|
toolCalls = Array.isArray(payload.toolCalls) ? payload.toolCalls : [];
|
|
@@ -19407,10 +19614,19 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
19407
19614
|
tab.selectedModelId = lockedModelId;
|
|
19408
19615
|
tab.promptSent = true;
|
|
19409
19616
|
if (!requestHolder.preserveComposer) clearAiChatTabComposer(tab);
|
|
19410
|
-
|
|
19617
|
+
let existingUserMessage = [...tab.feed.querySelectorAll(".user-message[data-message-id]")]
|
|
19411
19618
|
.find((candidate) => candidate.dataset.messageId === String(persistedUserMessage.id));
|
|
19412
|
-
if (
|
|
19413
|
-
|
|
19619
|
+
if (requestHolder.optimisticUserMessage?.isConnected) {
|
|
19620
|
+
if (existingUserMessage && existingUserMessage !== requestHolder.optimisticUserMessage) {
|
|
19621
|
+
requestHolder.optimisticUserMessage.remove();
|
|
19622
|
+
} else {
|
|
19623
|
+
const confirmedUserMessage = appendMessage("user", persistedUserMessage.content, persistedUserMessage.citations, persistedUserMessage.createdAt, persistedUserMessage.metadata, persistedUserMessage.id, { tab });
|
|
19624
|
+
requestHolder.optimisticUserMessage.replaceWith(confirmedUserMessage);
|
|
19625
|
+
requestHolder.optimisticUserMessage = confirmedUserMessage;
|
|
19626
|
+
existingUserMessage = confirmedUserMessage;
|
|
19627
|
+
}
|
|
19628
|
+
} else if (!existingUserMessage) {
|
|
19629
|
+
existingUserMessage = appendMessage("user", persistedUserMessage.content, persistedUserMessage.citations, persistedUserMessage.createdAt, persistedUserMessage.metadata, persistedUserMessage.id, { tab });
|
|
19414
19630
|
}
|
|
19415
19631
|
if (isActiveAiChatTab(tab)) {
|
|
19416
19632
|
state.aiConversationModelId = lockedModelId;
|
|
@@ -19520,6 +19736,7 @@ async function streamChat(requestHolder, body, idempotencyKey, { endpoint = null
|
|
|
19520
19736
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
19521
19737
|
if (streamError) throw streamError;
|
|
19522
19738
|
assertAiStreamCompleted(streamCompleted);
|
|
19739
|
+
if (warningOnly && messageMounted) message.remove();
|
|
19523
19740
|
return { action: warningOnly ? "warn" : contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, userMessage: persistedUserMessage, question };
|
|
19524
19741
|
} catch (error) {
|
|
19525
19742
|
const streamFailure = error instanceof Error ? error : new Error(String(error ?? "AI 流式调用失败"));
|
|
@@ -21683,8 +21900,8 @@ $("#ai-scope").addEventListener("change", (event) => {
|
|
|
21683
21900
|
setAiContextMeter(null);
|
|
21684
21901
|
});
|
|
21685
21902
|
$("#ai-mention-menu").addEventListener("click", (event) => {
|
|
21686
|
-
const
|
|
21687
|
-
if (
|
|
21903
|
+
const commandButton = event.target.closest("[data-ai-command-name]");
|
|
21904
|
+
if (commandButton) return selectAiSlashCommand(commandButton);
|
|
21688
21905
|
const button = event.target.closest("[data-ai-reference-id]");
|
|
21689
21906
|
if (button) selectAiMention(button);
|
|
21690
21907
|
});
|
|
@@ -21983,36 +22200,7 @@ $("#ai-new-conversation").addEventListener("click", async () => {
|
|
|
21983
22200
|
}
|
|
21984
22201
|
});
|
|
21985
22202
|
$("#ai-context-compact").addEventListener("click", async () => {
|
|
21986
|
-
|
|
21987
|
-
const modelId = $("#ai-model").value;
|
|
21988
|
-
if (!requestScope || !modelId) return toast("请先选择章节和模型", "error");
|
|
21989
|
-
const button = $("#ai-context-compact");
|
|
21990
|
-
setAiContextWarningActionsDisabled(true);
|
|
21991
|
-
button.textContent = "压缩中";
|
|
21992
|
-
try {
|
|
21993
|
-
const conversationId = await ensureAiConversation();
|
|
21994
|
-
const result = await api(`/api/ai-conversations/${conversationId}/compact`, {
|
|
21995
|
-
method: "POST",
|
|
21996
|
-
body: { modelId, scope: requestScope.scope }
|
|
21997
|
-
});
|
|
21998
|
-
hideAiContextWarning();
|
|
21999
|
-
$("#ai-prompt").focus();
|
|
22000
|
-
toast(result.changed ? `已整理 ${result.compactedMessageCount} 条较早消息为长期记忆` : "当前没有需要整理的较早消息");
|
|
22001
|
-
setAiContextMeter(result.usage);
|
|
22002
|
-
if (result.changed) {
|
|
22003
|
-
const current = state.aiConversations.find((conversation) => conversation.id === conversationId);
|
|
22004
|
-
if (current) {
|
|
22005
|
-
current.compactedMessageCount = result.compactedMessageCount;
|
|
22006
|
-
current.hasCompactedSummary = true;
|
|
22007
|
-
}
|
|
22008
|
-
renderConversationCompactionDivider(result.compactedMessageCount, current?.messageCount);
|
|
22009
|
-
}
|
|
22010
|
-
} catch (error) {
|
|
22011
|
-
toast(`上下文压缩失败:${error.message}`, "error");
|
|
22012
|
-
} finally {
|
|
22013
|
-
setAiContextWarningActionsDisabled(false);
|
|
22014
|
-
button.textContent = "压缩";
|
|
22015
|
-
}
|
|
22203
|
+
void compactAiConversation({ button: $("#ai-context-compact") });
|
|
22016
22204
|
});
|
|
22017
22205
|
$("#ai-context-new-conversation").addEventListener("click", async () => {
|
|
22018
22206
|
setAiContextWarningActionsDisabled(true);
|
|
@@ -22285,7 +22473,7 @@ $("#ai-prompt").addEventListener("keydown", (event) => {
|
|
|
22285
22473
|
const activeOption = $("#ai-mention-menu").querySelector('[role="option"][aria-selected="true"]');
|
|
22286
22474
|
if (activeOption) {
|
|
22287
22475
|
event.preventDefault();
|
|
22288
|
-
if (activeOption.dataset.
|
|
22476
|
+
if (activeOption.dataset.aiCommandName) selectAiSlashCommand(activeOption);
|
|
22289
22477
|
else selectAiMention(activeOption);
|
|
22290
22478
|
return;
|
|
22291
22479
|
}
|