@musnows/scriverse 0.7.4 → 0.7.6
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 +49 -7
- package/dist/ai.js.map +1 -1
- package/dist/app.js +44 -45
- package/dist/app.js.map +1 -1
- package/dist/epub-export.js +42 -38
- package/dist/epub-export.js.map +1 -1
- package/dist/public/app.js +127 -54
- package/dist/public/index.html +6 -6
- package/dist/public/outline-board.d.ts +16 -10
- package/dist/public/outline-board.js +11 -105
- package/dist/public/reading-preview.d.ts +5 -1
- package/dist/public/reading-preview.js +17 -2
- package/dist/public/styles.css +11 -8
- package/dist/store.js +239 -63
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/zip-stream.js +149 -0
- package/dist/zip-stream.js.map +1 -0
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -53,15 +53,18 @@ import {
|
|
|
53
53
|
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260812-reader-preview-v1";
|
|
54
54
|
import {
|
|
55
55
|
READING_PREFERENCES_STORAGE_KEY,
|
|
56
|
+
READING_PREFERENCES_VERSION,
|
|
56
57
|
adjacentReadingChapter,
|
|
57
58
|
buildReadingChapterSequence,
|
|
58
59
|
createReadingRequestGate,
|
|
59
60
|
normalizeReadingPosition,
|
|
60
61
|
normalizeReadingPreferences,
|
|
62
|
+
normalizeStoredReadingPreferences,
|
|
61
63
|
readingPositionStorageKey,
|
|
62
64
|
resolvePagedReadingStep,
|
|
65
|
+
resolveReadingTheme,
|
|
63
66
|
resolveReadingStart
|
|
64
|
-
} from "/reading-preview.js?v=
|
|
67
|
+
} from "/reading-preview.js?v=20260813-reader-theme-v2";
|
|
65
68
|
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
66
69
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
67
70
|
import { buildRaceForest, eligibleRaceParents, orderRaceFilterOptions, racePathLabel } from "/race-hierarchy.js?v=20260729-race-tree-all-v1";
|
|
@@ -81,9 +84,9 @@ import {
|
|
|
81
84
|
} from "/timeline-view.js?v=20260801-timeline-sort-actions-v1";
|
|
82
85
|
import {
|
|
83
86
|
normalizeOutlineBoardState,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
} from "/outline-board.js?v=
|
|
87
|
+
outlineBoardRequestPath,
|
|
88
|
+
outlineBoardUnresolvedCount
|
|
89
|
+
} from "/outline-board.js?v=20260813-outline-board-page-v1";
|
|
87
90
|
import { backgroundTaskActivityCount, backgroundTaskPollDelay, collectBackgroundTaskTransitions } from "/background-task-center.js?v=20260810-analysis-task-failed-v1";
|
|
88
91
|
import { createModuleRequestCache } from "/module-request-cache.js?v=20260730-module-request-cache-v1";
|
|
89
92
|
import { systemStatusPresentation } from "/system-status.js?v=20260801-system-health-v1";
|
|
@@ -1225,6 +1228,7 @@ let settingFiltersPanelOpen = false;
|
|
|
1225
1228
|
const outlineBoardFilters = normalizeOutlineBoardState();
|
|
1226
1229
|
let outlineBoardFiltersPanelOpen = false;
|
|
1227
1230
|
let outlineBoardRenderRequestId = 0;
|
|
1231
|
+
let outlineBoardSearchTimer = null;
|
|
1228
1232
|
const relationshipFilters = { fromCharacterIds: [], toCharacterIds: [] };
|
|
1229
1233
|
let relationshipFiltersPanelOpen = false;
|
|
1230
1234
|
|
|
@@ -1805,7 +1809,7 @@ function aiAssistantLabel(suffix = "") {
|
|
|
1805
1809
|
return suffix ? `${name} · ${suffix}` : name;
|
|
1806
1810
|
}
|
|
1807
1811
|
|
|
1808
|
-
function createAiContextCompactionDivider({ kind = "conversation", ariaLabel = "
|
|
1812
|
+
function createAiContextCompactionDivider({ kind = "conversation", ariaLabel = "当前上下文已压缩", title = "" } = {}) {
|
|
1809
1813
|
const divider = document.createElement("div");
|
|
1810
1814
|
divider.className = "ai-context-compaction-divider";
|
|
1811
1815
|
divider.dataset.contextCompaction = kind;
|
|
@@ -1813,7 +1817,7 @@ function createAiContextCompactionDivider({ kind = "conversation", ariaLabel = "
|
|
|
1813
1817
|
divider.setAttribute("role", "separator");
|
|
1814
1818
|
divider.setAttribute("aria-label", ariaLabel);
|
|
1815
1819
|
if (title) divider.title = title;
|
|
1816
|
-
divider.innerHTML = "<span
|
|
1820
|
+
divider.innerHTML = "<span>—— 当前上下文已压缩 ——</span>";
|
|
1817
1821
|
return divider;
|
|
1818
1822
|
}
|
|
1819
1823
|
|
|
@@ -5624,6 +5628,8 @@ function resetWorkScopedUiCaches() {
|
|
|
5624
5628
|
Object.assign(outlineBoardFilters, normalizeOutlineBoardState());
|
|
5625
5629
|
outlineBoardFiltersPanelOpen = false;
|
|
5626
5630
|
outlineBoardRenderRequestId += 1;
|
|
5631
|
+
clearTimeout(outlineBoardSearchTimer);
|
|
5632
|
+
outlineBoardSearchTimer = null;
|
|
5627
5633
|
chapterSelectionRequestGeneration += 1;
|
|
5628
5634
|
Object.keys(moduleListPages).forEach((key) => { moduleListPages[key] = 1; });
|
|
5629
5635
|
relationshipFilters.fromCharacterIds = [];
|
|
@@ -5797,8 +5803,8 @@ function renderTree() {
|
|
|
5797
5803
|
return `
|
|
5798
5804
|
<div class="volume-node ${collapsed ? "is-collapsed" : ""}" data-volume-id="${esc(volume.id)}">
|
|
5799
5805
|
<div class="volume-title">
|
|
5800
|
-
<button class="volume-toggle" type="button" data-volume-toggle="${esc(volume.id)}" aria-expanded="${collapsed ? "false" : "true"}" title="
|
|
5801
|
-
|
|
5806
|
+
<button class="volume-toggle" type="button" data-volume-toggle="${esc(volume.id)}" aria-expanded="${collapsed ? "false" : "true"}" title="左键展开或折叠;右键打开分卷详情;可将章节拖到这里追加"><span>${esc(volume.title)}</span><span>${Number(volume.chapterCount ?? chapters.length)} 章</span></button>
|
|
5807
|
+
${proseEditable ? `<button class="ghost-button volume-detail-button" type="button" data-volume-detail="${esc(volume.id)}" aria-label="打开“${esc(volume.title)}”分卷详情">详情</button>` : ""}
|
|
5802
5808
|
${proseEditable ? `<button class="add-button chapter-add-button" type="button" data-new-chapter-volume="${esc(volume.id)}" aria-label="在“${esc(volume.title)}”中新建章节" title="在“${esc(volume.title)}”中新建章节">+</button>` : ""}
|
|
5803
5809
|
</div>
|
|
5804
5810
|
<div class="volume-chapters">
|
|
@@ -5820,6 +5826,11 @@ function renderTree() {
|
|
|
5820
5826
|
event.preventDefault();
|
|
5821
5827
|
openVolumeDialog(state.work.volumes.find((volume) => volume.id === button.dataset.volumeToggle));
|
|
5822
5828
|
});
|
|
5829
|
+
button.addEventListener("keydown", (event) => {
|
|
5830
|
+
if (!canEditProse() || (event.key !== "ContextMenu" && !(event.shiftKey && event.key === "F10"))) return;
|
|
5831
|
+
event.preventDefault();
|
|
5832
|
+
openVolumeDialog(state.work.volumes.find((volume) => volume.id === button.dataset.volumeToggle));
|
|
5833
|
+
});
|
|
5823
5834
|
if (proseEditable) {
|
|
5824
5835
|
button.addEventListener("dragover", (event) => {
|
|
5825
5836
|
if (!event.dataTransfer?.types.includes("text/plain")) return;
|
|
@@ -5836,15 +5847,14 @@ function renderTree() {
|
|
|
5836
5847
|
});
|
|
5837
5848
|
}
|
|
5838
5849
|
});
|
|
5839
|
-
$("#novel-tree").querySelectorAll("[data-
|
|
5840
|
-
button.addEventListener("click", () => openChapterDialog(button.dataset.newChapterVolume));
|
|
5841
|
-
});
|
|
5842
|
-
$("#novel-tree").querySelectorAll("[data-export-volume]").forEach((button) => {
|
|
5850
|
+
$("#novel-tree").querySelectorAll("[data-volume-detail]").forEach((button) => {
|
|
5843
5851
|
button.addEventListener("click", () => {
|
|
5844
|
-
|
|
5845
|
-
if (volume) void downloadVolumeEpub(volume, button);
|
|
5852
|
+
openVolumeDialog(state.work.volumes.find((volume) => volume.id === button.dataset.volumeDetail));
|
|
5846
5853
|
});
|
|
5847
5854
|
});
|
|
5855
|
+
$("#novel-tree").querySelectorAll("[data-new-chapter-volume]").forEach((button) => {
|
|
5856
|
+
button.addEventListener("click", () => openChapterDialog(button.dataset.newChapterVolume));
|
|
5857
|
+
});
|
|
5848
5858
|
$("#novel-tree").querySelectorAll("[data-chapter-id]").forEach((button) => {
|
|
5849
5859
|
button.addEventListener("click", async () => {
|
|
5850
5860
|
const chapterId = button.dataset.chapterId;
|
|
@@ -6370,7 +6380,7 @@ function storedReadingPosition() {
|
|
|
6370
6380
|
}
|
|
6371
6381
|
|
|
6372
6382
|
function persistReadingPreferences() {
|
|
6373
|
-
try { localStorage.setItem(READING_PREFERENCES_STORAGE_KEY, JSON.stringify(readingPreferences)); } catch { /* 禁用本地存储时保留本次会话设置 */ }
|
|
6383
|
+
try { localStorage.setItem(READING_PREFERENCES_STORAGE_KEY, JSON.stringify({ ...readingPreferences, version: READING_PREFERENCES_VERSION })); } catch { /* 禁用本地存储时保留本次会话设置 */ }
|
|
6374
6384
|
}
|
|
6375
6385
|
|
|
6376
6386
|
function readingProgressRatio() {
|
|
@@ -6482,7 +6492,7 @@ function renderReadingNavigation() {
|
|
|
6482
6492
|
function applyReadingPreferences() {
|
|
6483
6493
|
const view = $("#reader-view");
|
|
6484
6494
|
const viewport = $("#reader-viewport");
|
|
6485
|
-
view.dataset.readerTheme = readingPreferences.theme;
|
|
6495
|
+
view.dataset.readerTheme = resolveReadingTheme(readingPreferences.theme, currentColorTheme());
|
|
6486
6496
|
view.style.setProperty("--reader-font-size", `${readingPreferences.fontSize}px`);
|
|
6487
6497
|
view.style.setProperty("--reader-line-height", String(readingPreferences.lineHeight));
|
|
6488
6498
|
viewport.classList.toggle("is-paged", readingPreferences.mode === "paged");
|
|
@@ -6738,7 +6748,7 @@ async function openReadingPreview({ chapterId = null, volumeId = null, restorePo
|
|
|
6738
6748
|
toast("当前作品还没有可阅读的章节", "error");
|
|
6739
6749
|
return false;
|
|
6740
6750
|
}
|
|
6741
|
-
readingPreferences =
|
|
6751
|
+
readingPreferences = normalizeStoredReadingPreferences(readReadingStorage(READING_PREFERENCES_STORAGE_KEY));
|
|
6742
6752
|
const saved = restorePosition ? storedReadingPosition() : null;
|
|
6743
6753
|
const target = resolveReadingStart(readingSequence, { chapterId, volumeId, storedPosition: saved });
|
|
6744
6754
|
if (!target) return false;
|
|
@@ -8191,16 +8201,17 @@ function bindOutlineBoardCards(board, workId) {
|
|
|
8191
8201
|
}));
|
|
8192
8202
|
}
|
|
8193
8203
|
|
|
8194
|
-
async function renderOutlines(foreshadowPage = moduleListPages.foreshadows) {
|
|
8204
|
+
async function renderOutlines(foreshadowPage = moduleListPages.foreshadows, boardPage = moduleListPages.outlinePlans) {
|
|
8195
8205
|
const workId = state.work.id;
|
|
8196
8206
|
const generation = workScopedUiGeneration;
|
|
8197
8207
|
const requestId = ++outlineBoardRenderRequestId;
|
|
8198
8208
|
const currentChapterId = state.chapter?.id;
|
|
8209
|
+
const boardPageSize = pageSizeFor("outlines");
|
|
8199
8210
|
let board;
|
|
8200
8211
|
let foreshadows;
|
|
8201
8212
|
try {
|
|
8202
8213
|
[board, foreshadows] = await Promise.all([
|
|
8203
|
-
moduleApi("outlines",
|
|
8214
|
+
moduleApi("outlines", outlineBoardRequestPath(workId, outlineBoardFilters, boardPage, boardPageSize)),
|
|
8204
8215
|
moduleApiAllPages("outlines", `/api/works/${encodeURIComponent(workId)}/foreshadows?status=all${currentChapterId ? `¤tChapterId=${encodeURIComponent(currentChapterId)}` : ""}`)
|
|
8205
8216
|
]);
|
|
8206
8217
|
} catch (error) {
|
|
@@ -8209,9 +8220,12 @@ async function renderOutlines(foreshadowPage = moduleListPages.foreshadows) {
|
|
|
8209
8220
|
}
|
|
8210
8221
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration || requestId !== outlineBoardRenderRequestId || state.module !== "outlines") return;
|
|
8211
8222
|
|
|
8212
|
-
if (outlineBoardFilters.volumeId && !board.
|
|
8223
|
+
if (outlineBoardFilters.volumeId && !board.volumeOptions.some((volume) => String(volume.id) === outlineBoardFilters.volumeId)) {
|
|
8213
8224
|
outlineBoardFilters.volumeId = "";
|
|
8225
|
+
board = await moduleApi("outlines", outlineBoardRequestPath(workId, outlineBoardFilters, 1, boardPageSize));
|
|
8226
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration || requestId !== outlineBoardRenderRequestId || state.module !== "outlines") return;
|
|
8214
8227
|
}
|
|
8228
|
+
moduleListPages.outlinePlans = board.page;
|
|
8215
8229
|
const foreshadowPageResult = paginateModuleItems(foreshadows, foreshadowPage, "outlines");
|
|
8216
8230
|
moduleListPages.foreshadows = foreshadowPageResult.page;
|
|
8217
8231
|
const layout = readModuleLayout();
|
|
@@ -8247,9 +8261,9 @@ async function renderOutlines(foreshadowPage = moduleListPages.foreshadows) {
|
|
|
8247
8261
|
|
|
8248
8262
|
const filterState = normalizeOutlineBoardState(outlineBoardFilters);
|
|
8249
8263
|
Object.assign(outlineBoardFilters, filterState);
|
|
8250
|
-
const volumeOptions = board.
|
|
8264
|
+
const volumeOptions = board.volumeOptions.map((volume) => `<option value="${esc(volume.id)}" ${filterState.volumeId === String(volume.id) ? "selected" : ""}>${esc(volume.title)}</option>`).join("");
|
|
8251
8265
|
const filterToolbar = `<section id="outline-board-filter-panel" class="character-filter-toolbar outline-board-filter-toolbar${outlineBoardFiltersPanelOpen ? "" : " hidden"}" aria-label="章节大纲看板筛选">
|
|
8252
|
-
<label class="setting-filter-field outline-board-search-field" for="outline-board-search"><span>搜索章节与摘要</span><input id="outline-board-search" type="search" value="${esc(filterState.query)}" placeholder="章节、目标、冲突、转折或伏笔" autocomplete="off"></label>
|
|
8266
|
+
<label class="setting-filter-field outline-board-search-field" for="outline-board-search"><span>搜索章节与摘要</span><input id="outline-board-search" type="search" value="${esc(filterState.query)}" placeholder="章节、目标、冲突、转折或伏笔" autocomplete="off" maxlength="200"></label>
|
|
8253
8267
|
<label class="setting-filter-field" for="outline-board-volume-filter"><span>按分卷筛选</span><select id="outline-board-volume-filter"><option value="">全部分卷</option>${volumeOptions}</select></label>
|
|
8254
8268
|
<label class="setting-filter-field" for="outline-board-status-filter"><span>按大纲状态筛选</span><select id="outline-board-status-filter"><option value="all" ${filterState.outlineStatus === "all" ? "selected" : ""}>全部大纲状态</option><option value="empty" ${filterState.outlineStatus === "empty" ? "selected" : ""}>尚未规划</option><option value="draft" ${filterState.outlineStatus === "draft" ? "selected" : ""}>草稿</option><option value="ready" ${filterState.outlineStatus === "ready" ? "selected" : ""}>可执行</option><option value="completed" ${filterState.outlineStatus === "completed" ? "selected" : ""}>已完成</option></select></label>
|
|
8255
8269
|
<label class="setting-filter-field" for="outline-board-foreshadow-filter"><span>按伏笔状态筛选</span><select id="outline-board-foreshadow-filter"><option value="all" ${filterState.foreshadowStatus === "all" ? "selected" : ""}>全部伏笔状态</option><option value="none" ${filterState.foreshadowStatus === "none" ? "selected" : ""}>无关联伏笔</option><option value="unresolved" ${filterState.foreshadowStatus === "unresolved" ? "selected" : ""}>有未回收伏笔</option><option value="resolved" ${filterState.foreshadowStatus === "resolved" ? "selected" : ""}>有已回收伏笔</option><option value="abandoned" ${filterState.foreshadowStatus === "abandoned" ? "selected" : ""}>有已放弃伏笔</option></select></label>
|
|
@@ -8259,21 +8273,44 @@ async function renderOutlines(foreshadowPage = moduleListPages.foreshadows) {
|
|
|
8259
8273
|
$("#module-content").innerHTML = `${filterToolbar}<div id="outline-board-results"></div><section class="planning-section outline-foreshadow-library"><div class="section-title"><div><span class="eyebrow">伏笔追踪</span><h2>尚未回收与历史伏笔</h2></div></div>${foreshadowHtml}</section>`;
|
|
8260
8274
|
mountOutlineBoardFilterToggle();
|
|
8261
8275
|
|
|
8262
|
-
const renderOutlineBoardResults = () => {
|
|
8263
|
-
const
|
|
8264
|
-
const
|
|
8265
|
-
const
|
|
8266
|
-
const
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8276
|
+
const renderOutlineBoardResults = (pageResult) => {
|
|
8277
|
+
const currentFilters = normalizeOutlineBoardState(outlineBoardFilters);
|
|
8278
|
+
const filtersActive = Boolean(currentFilters.query.trim() || currentFilters.volumeId || currentFilters.outlineStatus !== "all" || currentFilters.foreshadowStatus !== "all");
|
|
8279
|
+
const controlsActive = filtersActive || currentFilters.sort !== "tree";
|
|
8280
|
+
const summary = `<div class="outline-summary"><article><strong>${filtersActive ? pageResult.total : pageResult.stats.chapterCount}</strong><span>${filtersActive ? `筛选结果 / 共 ${pageResult.stats.chapterCount} 章` : "全书章节"}</span></article><article><strong>${pageResult.stats.outlinedChapterCount}</strong><span>已有章节大纲</span></article><article class="${pageResult.stats.unresolvedForeshadowCount ? "danger-text" : ""}"><strong>${pageResult.stats.unresolvedForeshadowCount}</strong><span>未回收伏笔</span></article></div>`;
|
|
8281
|
+
const boardHtml = pageResult.volumes.length
|
|
8282
|
+
? `<div class="outline-board">${pageResult.volumes.map((volume) => `<section class="outline-board-volume" data-outline-board-volume="${esc(volume.id)}" aria-labelledby="outline-board-volume-${esc(volume.id)}"><header><div><span class="eyebrow">分卷</span><h3 id="outline-board-volume-${esc(volume.id)}">${esc(volume.title)}</h3></div><span>${volume.chapters.length === volume.filteredChapterCount ? `${volume.chapters.length} 章` : `本页 ${volume.chapters.length} / ${volume.filteredChapterCount} 章`}</span></header>${volume.chapters.length ? `<div class="outline-board-grid">${volume.chapters.map((chapter) => outlineBoardCard(chapter, volume)).join("")}</div>` : '<div class="outline-board-volume-empty"><b>本卷暂无章节</b><span>空分卷会保留在全书看板中。</span></div>'}</section>`).join("")}</div>`
|
|
8283
|
+
: emptyModule(pageResult.stats.chapterCount ? "没有符合筛选条件的章节" : pageResult.volumeOptions.length ? "当前分卷还没有章节" : "还没有分卷", pageResult.stats.chapterCount ? "可以调整关键词、状态、分卷或排序条件。" : "先在作品目录创建分卷和章节,再维护每章目标、冲突与转折。");
|
|
8284
|
+
const pagination = renderModulePagination(pageResult, "outlinePlans", "章节大纲看板");
|
|
8285
|
+
$("#outline-board-results").innerHTML = `${summary}<section class="planning-section"><div class="section-title"><div><span class="eyebrow">全书总览</span><h2>章节大纲看板</h2></div><span class="outline-board-result-note">本页 ${pageResult.itemCount} / ${pageResult.total} 章</span></div>${boardHtml}${pagination}</section>`;
|
|
8286
|
+
$("#outline-board-filter-count").textContent = filtersActive ? `筛选后共 ${pageResult.total} 章` : "";
|
|
8271
8287
|
$("#clear-outline-board-filters").disabled = !controlsActive;
|
|
8272
|
-
mountModuleCount(
|
|
8273
|
-
bindOutlineBoardCards(
|
|
8288
|
+
mountModuleCount(pageResult.total + foreshadows.length);
|
|
8289
|
+
bindOutlineBoardCards(pageResult, workId);
|
|
8290
|
+
bindModulePagination("outlinePlans", (page) => refreshOutlineBoard(page));
|
|
8291
|
+
};
|
|
8292
|
+
|
|
8293
|
+
const refreshOutlineBoard = async (page = 1) => {
|
|
8294
|
+
const pageRequestId = ++outlineBoardRenderRequestId;
|
|
8295
|
+
moduleListPages.outlinePlans = page;
|
|
8296
|
+
$("#outline-board-results")?.setAttribute("aria-busy", "true");
|
|
8297
|
+
$("#outline-board-filter-count").textContent = "正在加载看板…";
|
|
8298
|
+
try {
|
|
8299
|
+
const nextBoard = await moduleApi("outlines", outlineBoardRequestPath(workId, outlineBoardFilters, page, boardPageSize));
|
|
8300
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration || pageRequestId !== outlineBoardRenderRequestId || state.module !== "outlines") return;
|
|
8301
|
+
board = nextBoard;
|
|
8302
|
+
moduleListPages.outlinePlans = nextBoard.page;
|
|
8303
|
+
renderOutlineBoardResults(nextBoard);
|
|
8304
|
+
} catch (error) {
|
|
8305
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration || pageRequestId !== outlineBoardRenderRequestId || state.module !== "outlines") return;
|
|
8306
|
+
$("#outline-board-filter-count").textContent = "看板加载失败";
|
|
8307
|
+
toast(`读取章节大纲看板失败:${error.message}`, "error");
|
|
8308
|
+
} finally {
|
|
8309
|
+
if (pageRequestId === outlineBoardRenderRequestId) $("#outline-board-results")?.removeAttribute("aria-busy");
|
|
8310
|
+
}
|
|
8274
8311
|
};
|
|
8275
8312
|
|
|
8276
|
-
const updateFilters = () => {
|
|
8313
|
+
const updateFilters = (defer = false) => {
|
|
8277
8314
|
Object.assign(outlineBoardFilters, normalizeOutlineBoardState({
|
|
8278
8315
|
query: $("#outline-board-search").value,
|
|
8279
8316
|
volumeId: $("#outline-board-volume-filter").value,
|
|
@@ -8282,10 +8319,16 @@ async function renderOutlines(foreshadowPage = moduleListPages.foreshadows) {
|
|
|
8282
8319
|
sort: $("#outline-board-sort").value
|
|
8283
8320
|
}));
|
|
8284
8321
|
outlineBoardFiltersPanelOpen = true;
|
|
8285
|
-
|
|
8322
|
+
clearTimeout(outlineBoardSearchTimer);
|
|
8323
|
+
if (defer) {
|
|
8324
|
+
outlineBoardSearchTimer = window.setTimeout(() => { void refreshOutlineBoard(1); }, 250);
|
|
8325
|
+
} else {
|
|
8326
|
+
outlineBoardSearchTimer = null;
|
|
8327
|
+
void refreshOutlineBoard(1);
|
|
8328
|
+
}
|
|
8286
8329
|
};
|
|
8287
|
-
$("#outline-board-search").addEventListener("input", updateFilters);
|
|
8288
|
-
["#outline-board-volume-filter", "#outline-board-status-filter", "#outline-board-foreshadow-filter", "#outline-board-sort"].forEach((selector) => $(selector).addEventListener("change", updateFilters));
|
|
8330
|
+
$("#outline-board-search").addEventListener("input", () => updateFilters(true));
|
|
8331
|
+
["#outline-board-volume-filter", "#outline-board-status-filter", "#outline-board-foreshadow-filter", "#outline-board-sort"].forEach((selector) => $(selector).addEventListener("change", () => updateFilters(false)));
|
|
8289
8332
|
$("#clear-outline-board-filters").addEventListener("click", () => {
|
|
8290
8333
|
Object.assign(outlineBoardFilters, normalizeOutlineBoardState());
|
|
8291
8334
|
$("#outline-board-search").value = "";
|
|
@@ -8294,10 +8337,12 @@ async function renderOutlines(foreshadowPage = moduleListPages.foreshadows) {
|
|
|
8294
8337
|
$("#outline-board-foreshadow-filter").value = "all";
|
|
8295
8338
|
$("#outline-board-sort").value = "tree";
|
|
8296
8339
|
outlineBoardFiltersPanelOpen = true;
|
|
8297
|
-
renderOutlineBoardResults();
|
|
8298
8340
|
$("#outline-board-search").focus();
|
|
8341
|
+
clearTimeout(outlineBoardSearchTimer);
|
|
8342
|
+
outlineBoardSearchTimer = null;
|
|
8343
|
+
void refreshOutlineBoard(1);
|
|
8299
8344
|
});
|
|
8300
|
-
renderOutlineBoardResults();
|
|
8345
|
+
renderOutlineBoardResults(board);
|
|
8301
8346
|
bindModuleLayoutToggle(() => renderOutlines(foreshadowPageResult.page));
|
|
8302
8347
|
bindModulePagination("foreshadows", renderOutlines);
|
|
8303
8348
|
$("#module-content").querySelectorAll("[data-edit-foreshadow]").forEach((button) => button.addEventListener("click", () => openForeshadowDialog(foreshadows.find((item) => item.id === button.dataset.editForeshadow))));
|
|
@@ -10138,7 +10183,7 @@ async function renderBookAiSettings() {
|
|
|
10138
10183
|
host.innerHTML = `<section class="config-section">${tokenUsageOverviewMarkup(usage, {
|
|
10139
10184
|
title: "本书 Token 用量",
|
|
10140
10185
|
description: `仅统计《${state.work.title}》迄今产生的 AI Token 消耗与缓存命中情况。`
|
|
10141
|
-
})}</section><section class="config-section"><div class="config-section-header"><div><h2>每日 Token 额度</h2><p>限制本书在后端部署时区(${esc(quotaTimezone)})每个自然日可使用的输入与输出 Token 总量。额度最低为 10,000;达到额度后,新的 AI 请求会等到后端时区的次日零点重置后再执行。</p></div></div><div class="config-inline-save"><label class="checkbox-field config-checkbox-field"><input id="daily-token-quota-enabled" type="checkbox" ${dailyTokenQuota === null ? "" : "checked"}>启用每日额度</label><label class="daily-token-quota-field">每日额度<input id="daily-token-quota" type="number" min="10000" max="2000000000" step="1000" value="${esc(String(dailyTokenQuota ?? 10000))}" aria-label="本书每日 Token 额度" ${dailyTokenQuota === null ? "disabled" : ""}></label><button id="save-daily-token-quota" class="ghost-button config-save-button" type="button">保存</button></div><p id="daily-token-quota-status" class="usage-measurement-note" role="status">${esc(quotaStatusText)}</p></section><section class="config-section"><div class="config-section-header"><div><h2>本书系统提示词</h2><p>会追加在内置系统提示词和平台全局系统提示词之后,只影响《${esc(state.work.title)}》的 AI 请求。</p></div></div><div class="field-label"><textarea id="work-system-prompt" rows="8" aria-label="本书系统提示词" placeholder="例如:叙事使用第三人称,哥斯拉不得离开地球。">${esc(settings.systemPrompt)}</textarea></div><div class="card-actions"><button id="save-work-system-prompt" class="ghost-button config-save-button" type="button">保存本书提示词</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>人物关系拼音索引</h2><p>平时由系统记录增量任务;“同步增量队列”只处理发生变化的来源,“完整重建索引”会将本书全部正文和设定来源重新排队。</p></div></div><div id="relationship-search-index-status" role="status" aria-live="polite">${relationshipIndexStatusMarkup(relationshipIndex)}</div><div class="relationship-index-actions"><button id="sync-relationship-search-index" class="primary-button config-save-button" type="button">同步增量队列</button><button id="refresh-relationship-search-index" class="ghost-button" type="button">刷新状态</button><button id="rebuild-relationship-search-index" class="ghost-button config-save-button" type="button">完整重建索引</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>全书概要引用配额</h2><p>引用全书概要时按分卷保留覆盖,并优先加入与当前问题相关的章节概要;该比例控制概要可使用的上下文预算。</p></div></div><div class="config-inline-save"><label class="book-summary-context-percent-field">上下文占比(%)<input id="book-summary-context-percent" type="number" min="1" max="90" value="${esc(String(settings.bookSummaryContextPercent ?? 50))}" aria-label="全书概要引用上下文占比"></label><button id="save-book-summary-context-percent" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>对话上下文 Compact</h2><p
|
|
10186
|
+
})}</section><section class="config-section"><div class="config-section-header"><div><h2>每日 Token 额度</h2><p>限制本书在后端部署时区(${esc(quotaTimezone)})每个自然日可使用的输入与输出 Token 总量。额度最低为 10,000;达到额度后,新的 AI 请求会等到后端时区的次日零点重置后再执行。</p></div></div><div class="config-inline-save"><label class="checkbox-field config-checkbox-field"><input id="daily-token-quota-enabled" type="checkbox" ${dailyTokenQuota === null ? "" : "checked"}>启用每日额度</label><label class="daily-token-quota-field">每日额度<input id="daily-token-quota" type="number" min="10000" max="2000000000" step="1000" value="${esc(String(dailyTokenQuota ?? 10000))}" aria-label="本书每日 Token 额度" ${dailyTokenQuota === null ? "disabled" : ""}></label><button id="save-daily-token-quota" class="ghost-button config-save-button" type="button">保存</button></div><p id="daily-token-quota-status" class="usage-measurement-note" role="status">${esc(quotaStatusText)}</p></section><section class="config-section"><div class="config-section-header"><div><h2>本书系统提示词</h2><p>会追加在内置系统提示词和平台全局系统提示词之后,只影响《${esc(state.work.title)}》的 AI 请求。</p></div></div><div class="field-label"><textarea id="work-system-prompt" rows="8" aria-label="本书系统提示词" placeholder="例如:叙事使用第三人称,哥斯拉不得离开地球。">${esc(settings.systemPrompt)}</textarea></div><div class="card-actions"><button id="save-work-system-prompt" class="ghost-button config-save-button" type="button">保存本书提示词</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>人物关系拼音索引</h2><p>平时由系统记录增量任务;“同步增量队列”只处理发生变化的来源,“完整重建索引”会将本书全部正文和设定来源重新排队。</p></div></div><div id="relationship-search-index-status" role="status" aria-live="polite">${relationshipIndexStatusMarkup(relationshipIndex)}</div><div class="relationship-index-actions"><button id="sync-relationship-search-index" class="primary-button config-save-button" type="button">同步增量队列</button><button id="refresh-relationship-search-index" class="ghost-button" type="button">刷新状态</button><button id="rebuild-relationship-search-index" class="ghost-button config-save-button" type="button">完整重建索引</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>全书概要引用配额</h2><p>引用全书概要时按分卷保留覆盖,并优先加入与当前问题相关的章节概要;该比例控制概要可使用的上下文预算。</p></div></div><div class="config-inline-save"><label class="book-summary-context-percent-field">上下文占比(%)<input id="book-summary-context-percent" type="number" min="1" max="90" value="${esc(String(settings.bookSummaryContextPercent ?? 50))}" aria-label="全书概要引用上下文占比"></label><button id="save-book-summary-context-percent" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>对话上下文 Compact</h2><p>该阈值按对话历史的独立预算计算,用于显示可选择压缩或忽略的提醒;整次请求达到模型上下文窗口 95% 时仍会强制压缩较早消息,并尽量保留最近八条原文。</p></div></div><div class="config-inline-save"><label class="context-compact-threshold-field">Compact 阈值(%)<input id="context-compact-threshold" type="number" min="50" max="90" value="${esc(String(settings.contextCompactThreshold ?? 85))}" aria-label="对话上下文 compact 阈值"></label><button id="save-context-compact-threshold" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>设定上下文注入</h2><p>开启后,本书的普通 AI 请求会自动注入锁定设定、组织、种族与相关约束;即使本轮同时使用“@注入上下文设定”,也只会注入一次。</p></div></div><div class="config-inline-save"><label class="checkbox-field config-checkbox-field"><input id="always-include-setting-info" type="checkbox" ${settings.alwaysIncludeSettingInfo ? "checked" : ""}>是否注入设定</label><button id="save-always-include-setting-info" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section"><div class="config-section-header"><div><h2>Agent 工具调用上限</h2><p>限制单次回答里 Agent 可调用工具的次数,并用「全局倍数」给整次回答加一道不会因 Compact 重置的熔断阀,防止工具死循环空耗 Token。调用上限 5–48(默认 12);全局倍数 1–6(默认 3,全局上限 = 调用上限 × 倍数)。<a class="config-doc-link" href="https://scriverse.top/docs/global-tool-call-limit.html" target="_blank" rel="noopener noreferrer">了解原理与推荐设置</a></p></div></div><div class="config-inline-save"><label class="agent-tool-call-limit-field">调用上限<input id="agent-tool-call-limit" type="number" min="5" max="48" value="${esc(String(settings.agentToolCallLimit ?? 12))}" aria-label="Agent 工具调用上限"></label><div class="agent-tool-call-global-multiplier-field"><span id="agent-tool-call-global-multiplier-label">全局倍数</span><div class="settings-layout-toggle agent-tool-call-global-multiplier-toggle" role="group" aria-labelledby="agent-tool-call-global-multiplier-label">${[1, 2, 3, 4, 5, 6].map((value) => `<button type="button" data-global-multiplier="${value}" aria-pressed="${Number(settings.agentToolCallGlobalMultiplier ?? 3) === value}">${value}</button>`).join("")}</div><input id="agent-tool-call-global-multiplier" type="hidden" value="${esc(String(Math.min(6, Math.max(1, Number(settings.agentToolCallGlobalMultiplier ?? 3) || 3))))}" aria-label="Agent 工具调用全局倍数"></div><button id="save-agent-tool-call-limit" class="ghost-button config-save-button" type="button">保存</button></div></section><section class="config-section ai-agent-tools-section"><div class="config-section-header"><div><h2>AI 查询工具</h2><p>工具默认可用,作为已有上下文的补充。关闭后模型不会看到对应能力;所有工具只读且有数量、篇幅与调用轮次限制。已开始的对话会锁定创建时的工具集,修改后仅对新对话生效,避免打断 prompt cache。</p></div></div><div class="ai-agent-tools"><label><input name="agent-tool" type="checkbox" value="story_index" ${agentTools.has("story_index") ? "checked" : ""}><span><strong>作品目录与章节概要</strong><small>分页获取卷章、章节 ID 和当前概要,不返回正文。</small></span></label><label><input name="agent-tool" type="checkbox" value="read_chapters" ${agentTools.has("read_chapters") ? "checked" : ""}><span><strong>读取章节</strong><small>按章节 ID 获取概要或正文,每次最多 3 章。</small></span></label><label><input name="agent-tool" type="checkbox" value="search_story_entities" ${agentTools.has("search_story_entities") ? "checked" : ""}><span><strong>搜索作品实体</strong><small>按实体名、拼音或短关键词混合检索设定、人物、组织、时间线、关系、大纲和伏笔;非语义问答。</small></span></label></div><div class="card-actions"><button id="save-agent-tools" class="ghost-button config-save-button" type="button">保存工具设置</button></div></section>${renderTaskDefaults(models, providers, taskDefaults, settings)}`;
|
|
10142
10187
|
const agentToolCallLimitInput = host.querySelector("#agent-tool-call-limit");
|
|
10143
10188
|
agentToolCallLimitInput?.setAttribute("max", String(maximumAgentToolCallLimit));
|
|
10144
10189
|
const agentToolCallDescription = agentToolCallLimitInput?.closest(".config-section")?.querySelector(".config-section-header p");
|
|
@@ -10555,10 +10600,8 @@ function setAiContextDistributionVisible(visible) {
|
|
|
10555
10600
|
}
|
|
10556
10601
|
|
|
10557
10602
|
function showAiContextWarning(usage = null) {
|
|
10558
|
-
|
|
10559
|
-
|
|
10560
|
-
$("#ai-context-warning-title").textContent = percent ? `对话历史已使用 ${percent}% 的独立预算` : "对话历史接近整理阈值";
|
|
10561
|
-
$("#ai-context-warning-message").textContent = `已达到 ${threshold}% 的长期记忆整理阈值。现在可整理较早对话或新开对话;若继续发送,系统会先生成带来源的结构化长期记忆。作品正文超限不会触发此操作。`;
|
|
10603
|
+
$("#ai-context-warning-title").textContent = "对话上下文过长";
|
|
10604
|
+
$("#ai-context-warning-message").textContent = "当前对话上下文过长,是否进行压缩?不压缩可能会导致后续请求失败";
|
|
10562
10605
|
$("#ai-context-warning").classList.remove("hidden");
|
|
10563
10606
|
}
|
|
10564
10607
|
|
|
@@ -10566,6 +10609,10 @@ function hideAiContextWarning() {
|
|
|
10566
10609
|
$("#ai-context-warning").classList.add("hidden");
|
|
10567
10610
|
}
|
|
10568
10611
|
|
|
10612
|
+
function setAiContextWarningActionsDisabled(disabled) {
|
|
10613
|
+
for (const button of $("#ai-context-warning").querySelectorAll("button")) button.disabled = disabled;
|
|
10614
|
+
}
|
|
10615
|
+
|
|
10569
10616
|
async function loadAiReferences() {
|
|
10570
10617
|
const workId = state.work?.id;
|
|
10571
10618
|
if (!workId) return;
|
|
@@ -11138,11 +11185,11 @@ function openVolumeDialog(item) {
|
|
|
11138
11185
|
if (!state.work) return openWorkDialog();
|
|
11139
11186
|
if (!canEditProse()) return toast("当前权限只能编辑设定资料,不能修改分卷", "error");
|
|
11140
11187
|
const kindOptions = [["main", "正文卷"], ["prequel", "前传"], ["extra", "番外"], ["epilogue", "后记"], ["appendix", "附录"]];
|
|
11141
|
-
const management = item ? `<section class="entity-dialog-management" aria-label="
|
|
11142
|
-
<div><strong
|
|
11143
|
-
<div class="entity-dialog-management-actions"><button class="danger-button" type="button" data-dialog-volume-delete>移入回收站</button></div>
|
|
11188
|
+
const management = item ? `<section class="entity-dialog-management" aria-label="分卷详情操作">
|
|
11189
|
+
<div><strong>分卷详情</strong><small>可将当前分卷单独导出为 EPUB;移入回收站时会连同其中章节隐藏,正文、版本和关联资料默认保留 30 天。</small></div>
|
|
11190
|
+
<div class="entity-dialog-management-actions"><button class="ghost-button" type="button" data-dialog-volume-export>导出 EPUB</button><button class="danger-button" type="button" data-dialog-volume-delete>移入回收站</button></div>
|
|
11144
11191
|
</section>` : "";
|
|
11145
|
-
openDialog(item ? "
|
|
11192
|
+
openDialog(item ? "分卷详情" : "新建分卷",
|
|
11146
11193
|
field("title", "分卷名称", "text", item?.title) +
|
|
11147
11194
|
field("kind", "分卷类型", "select", item?.kind ?? "main", kindOptions) +
|
|
11148
11195
|
field("description", "分卷简介", "textarea", item?.description) +
|
|
@@ -11160,6 +11207,9 @@ function openVolumeDialog(item) {
|
|
|
11160
11207
|
renderTree();
|
|
11161
11208
|
toast(item ? "分卷设置已保存" : "分卷已创建");
|
|
11162
11209
|
}, "分卷设置");
|
|
11210
|
+
$("#dialog-fields").querySelector("[data-dialog-volume-export]")?.addEventListener("click", (event) => {
|
|
11211
|
+
void downloadVolumeEpub(item, event.currentTarget);
|
|
11212
|
+
});
|
|
11163
11213
|
$("#dialog-fields").querySelector("[data-dialog-volume-delete]")?.addEventListener("click", () => {
|
|
11164
11214
|
void deleteVolume(item);
|
|
11165
11215
|
});
|
|
@@ -13332,6 +13382,10 @@ function openModelDialog(providerId, item = null, provider = null) {
|
|
|
13332
13382
|
}
|
|
13333
13383
|
|
|
13334
13384
|
async function sendAi() {
|
|
13385
|
+
return sendAiWithOptions();
|
|
13386
|
+
}
|
|
13387
|
+
|
|
13388
|
+
async function sendAiWithOptions({ ignoreContextWarning = false } = {}) {
|
|
13335
13389
|
if (!state.work) return toast("请先选择作品", "error");
|
|
13336
13390
|
const composerSnapshot = captureAiPromptComposer();
|
|
13337
13391
|
const instruction = composerSnapshot.text.trim();
|
|
@@ -13404,7 +13458,8 @@ async function sendAi() {
|
|
|
13404
13458
|
scope,
|
|
13405
13459
|
modelId,
|
|
13406
13460
|
citations,
|
|
13407
|
-
conversationId: requestHolder.snapshot.conversationId
|
|
13461
|
+
conversationId: requestHolder.snapshot.conversationId,
|
|
13462
|
+
...(ignoreContextWarning ? { ignoreContextWarning: true } : {})
|
|
13408
13463
|
}, createAiIdempotencyKey());
|
|
13409
13464
|
const request = assertAiRequestCurrent(requestHolder.snapshot);
|
|
13410
13465
|
if (streamed.action === "warn") return;
|
|
@@ -13575,6 +13630,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
13575
13630
|
let conversationTitle = null;
|
|
13576
13631
|
let persistedUserMessage = null;
|
|
13577
13632
|
let contextAction = "ready";
|
|
13633
|
+
let warningOnly = false;
|
|
13578
13634
|
let streamContextCompacted = false;
|
|
13579
13635
|
let finalAnswerStarted = false;
|
|
13580
13636
|
const processStartedAt = Date.now();
|
|
@@ -13698,6 +13754,11 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
13698
13754
|
setAiContextMeter(payload.contextUsage);
|
|
13699
13755
|
if (messageMounted) meta.textContent = "已压缩工具上下文,正在继续生成";
|
|
13700
13756
|
} else if (eventName === "complete") {
|
|
13757
|
+
if (payload.warningOnly === true) {
|
|
13758
|
+
warningOnly = true;
|
|
13759
|
+
setAiContextMeter(payload.contextUsage);
|
|
13760
|
+
return;
|
|
13761
|
+
}
|
|
13701
13762
|
mountAssistantMessage();
|
|
13702
13763
|
persistedMessageId = typeof payload.messageId === "string" ? payload.messageId : null;
|
|
13703
13764
|
persistedMessageCreatedAt = typeof payload.messageCreatedAt === "string" ? payload.messageCreatedAt : null;
|
|
@@ -13729,7 +13790,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
|
|
|
13729
13790
|
assertAiRequestCurrent(requestHolder.snapshot);
|
|
13730
13791
|
if (streamError) throw streamError;
|
|
13731
13792
|
assertAiStreamCompleted(streamCompleted);
|
|
13732
|
-
return { action: contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, userMessage: persistedUserMessage };
|
|
13793
|
+
return { action: warningOnly ? "warn" : contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, userMessage: persistedUserMessage };
|
|
13733
13794
|
} catch (error) {
|
|
13734
13795
|
const streamFailure = error instanceof Error ? error : new Error(String(error ?? "AI 流式调用失败"));
|
|
13735
13796
|
const interruptionCode = typeof streamFailure.code === "string" ? streamFailure.code.slice(0, 100) : "AI_STREAM_FAILED";
|
|
@@ -15655,7 +15716,7 @@ $("#ai-context-compact").addEventListener("click", async () => {
|
|
|
15655
15716
|
const modelId = $("#ai-model").value;
|
|
15656
15717
|
if (!requestScope || !modelId) return toast("请先选择章节和模型", "error");
|
|
15657
15718
|
const button = $("#ai-context-compact");
|
|
15658
|
-
|
|
15719
|
+
setAiContextWarningActionsDisabled(true);
|
|
15659
15720
|
button.textContent = "压缩中";
|
|
15660
15721
|
try {
|
|
15661
15722
|
const conversationId = await ensureAiConversation();
|
|
@@ -15664,6 +15725,7 @@ $("#ai-context-compact").addEventListener("click", async () => {
|
|
|
15664
15725
|
body: { modelId, scope: requestScope.scope }
|
|
15665
15726
|
});
|
|
15666
15727
|
hideAiContextWarning();
|
|
15728
|
+
$("#ai-prompt").focus();
|
|
15667
15729
|
toast(result.changed ? `已整理 ${result.compactedMessageCount} 条较早消息为长期记忆` : "当前没有需要整理的较早消息");
|
|
15668
15730
|
setAiContextMeter(result.usage);
|
|
15669
15731
|
if (result.changed) {
|
|
@@ -15677,19 +15739,30 @@ $("#ai-context-compact").addEventListener("click", async () => {
|
|
|
15677
15739
|
} catch (error) {
|
|
15678
15740
|
toast(`上下文压缩失败:${error.message}`, "error");
|
|
15679
15741
|
} finally {
|
|
15680
|
-
|
|
15681
|
-
button.textContent = "
|
|
15742
|
+
setAiContextWarningActionsDisabled(false);
|
|
15743
|
+
button.textContent = "压缩";
|
|
15682
15744
|
}
|
|
15683
15745
|
});
|
|
15684
15746
|
$("#ai-context-new-conversation").addEventListener("click", async () => {
|
|
15747
|
+
setAiContextWarningActionsDisabled(true);
|
|
15685
15748
|
try {
|
|
15686
15749
|
await createNewAiConversation();
|
|
15687
15750
|
hideAiContextWarning();
|
|
15688
15751
|
} catch (error) {
|
|
15689
15752
|
toast(error.message, "error");
|
|
15753
|
+
} finally {
|
|
15754
|
+
setAiContextWarningActionsDisabled(false);
|
|
15755
|
+
}
|
|
15756
|
+
});
|
|
15757
|
+
$("#ai-context-dismiss").addEventListener("click", async () => {
|
|
15758
|
+
setAiContextWarningActionsDisabled(true);
|
|
15759
|
+
try {
|
|
15760
|
+
await sendAiWithOptions({ ignoreContextWarning: true });
|
|
15761
|
+
if ($("#ai-context-warning").classList.contains("hidden")) $("#ai-prompt").focus();
|
|
15762
|
+
} finally {
|
|
15763
|
+
setAiContextWarningActionsDisabled(false);
|
|
15690
15764
|
}
|
|
15691
15765
|
});
|
|
15692
|
-
$("#ai-context-dismiss").addEventListener("click", hideAiContextWarning);
|
|
15693
15766
|
$("#ai-history-toggle").addEventListener("click", async () => {
|
|
15694
15767
|
if ($("#ai-history-dialog").open) return setAiHistoryVisible(false);
|
|
15695
15768
|
try {
|
package/dist/public/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
12
|
<link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
|
|
13
|
-
<link rel="stylesheet" href="/styles.css?v=
|
|
13
|
+
<link rel="stylesheet" href="/styles.css?v=20260813-scrollbar-stable-v1">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -125,7 +125,6 @@
|
|
|
125
125
|
</div>
|
|
126
126
|
<nav id="module-nav" class="module-nav" aria-label="作品模块">
|
|
127
127
|
<button type="button" data-module="editor" class="active"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>正文</button>
|
|
128
|
-
<button id="reader-open-button" type="button" aria-label="打开沉浸式阅读预览" title="阅读预览"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 5.5A2.5 2.5 0 0 1 5.5 3H11v16H5.5A2.5 2.5 0 0 0 3 21.5Z"/><path d="M21 5.5A2.5 2.5 0 0 0 18.5 3H13v16h5.5a2.5 2.5 0 0 1 2.5 2.5Z"/></svg>阅读预览</button>
|
|
129
128
|
<button type="button" data-module="drafts"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20h4l11-11a2.8 2.8 0 0 0-4-4L4 16v4Z"/><path d="m13.5 6.5 4 4"/></svg>想法</button>
|
|
130
129
|
<button type="button" data-module="settings"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg>设定</button>
|
|
131
130
|
<button type="button" data-module="characters"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>角色</button>
|
|
@@ -135,6 +134,7 @@
|
|
|
135
134
|
<button type="button" data-module="races"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="4" r="2"/><circle cx="18" cy="8" r="2"/><circle cx="20" cy="16" r="2"/><path d="M9 10a5 5 0 0 1 5 5v3.5a3.5 3.5 0 0 1-6.84 1.045Q6.52 17.48 4.46 16.84A3.5 3.5 0 0 1 5.5 10Z"/></svg>种族</button>
|
|
136
135
|
<button class="ai-analysis-entry" type="button" data-module="tasks"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>AI 分析</button>
|
|
137
136
|
<button id="module-more-button" type="button" aria-expanded="false"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span class="nav-label">更多</span></button>
|
|
137
|
+
<button id="reader-open-button" class="module-nav-secondary hidden" type="button" aria-label="打开沉浸式阅读预览" title="阅读预览"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 5.5A2.5 2.5 0 0 1 5.5 3H11v16H5.5A2.5 2.5 0 0 0 3 21.5Z"/><path d="M21 5.5A2.5 2.5 0 0 0 18.5 3H13v16h5.5a2.5 2.5 0 0 1 2.5 2.5Z"/></svg>阅读预览</button>
|
|
138
138
|
<button class="module-nav-secondary hidden" type="button" data-module="comments"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4Z"/><path d="M8 9h8M8 13h5"/></svg><span class="nav-label">正文评论</span></button>
|
|
139
139
|
<button class="module-nav-secondary hidden" type="button" data-module="outlines"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></svg><span class="nav-label">大纲/伏笔</span></button>
|
|
140
140
|
<button class="module-nav-secondary hidden" type="button" data-module="reviews"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="m9 14 2 2 4-4"/></svg>审核</button>
|
|
@@ -331,8 +331,8 @@
|
|
|
331
331
|
<div class="prompt-box">
|
|
332
332
|
<div id="ai-citations" class="ai-citations hidden" aria-label="已添加的正文引用"></div>
|
|
333
333
|
<section id="ai-context-warning" class="ai-context-warning hidden" role="status" aria-live="polite">
|
|
334
|
-
<div><strong id="ai-context-warning-title"
|
|
335
|
-
<div><button id="ai-context-compact" type="button"
|
|
334
|
+
<div><strong id="ai-context-warning-title">对话上下文过长</strong><p id="ai-context-warning-message">当前对话上下文过长,是否进行压缩?不压缩可能会导致后续请求失败</p></div>
|
|
335
|
+
<div><button id="ai-context-compact" type="button">压缩</button><button id="ai-context-dismiss" type="button">忽略</button><button id="ai-context-new-conversation" type="button">新开对话</button></div>
|
|
336
336
|
</section>
|
|
337
337
|
<div class="prompt-options">
|
|
338
338
|
<select id="ai-task" aria-label="任务类型">
|
|
@@ -389,7 +389,7 @@
|
|
|
389
389
|
<option value="1.6">紧凑 1.6</option><option value="1.8">适中 1.8</option><option value="1.9">舒展 1.9</option><option value="2">宽松 2.0</option><option value="2.2">宽松 2.2</option>
|
|
390
390
|
</select></label>
|
|
391
391
|
<label>主题<select id="reader-theme" aria-label="阅读主题">
|
|
392
|
-
<option value="paper">纸张</option><option value="light">明亮</option><option value="dark">夜间</option>
|
|
392
|
+
<option value="auto">跟随工作台</option><option value="paper">纸张</option><option value="light">明亮</option><option value="dark">夜间</option>
|
|
393
393
|
</select></label>
|
|
394
394
|
</div>
|
|
395
395
|
</details>
|
|
@@ -1156,6 +1156,6 @@
|
|
|
1156
1156
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
1157
1157
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
1158
1158
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
1159
|
-
<script type="module" src="/app.js?v=
|
|
1159
|
+
<script type="module" src="/app.js?v=20260813-outline-board-page-v1"></script>
|
|
1160
1160
|
</body>
|
|
1161
1161
|
</html>
|
|
@@ -30,11 +30,21 @@ export type OutlineBoardVolume<T extends OutlineBoardChapter = OutlineBoardChapt
|
|
|
30
30
|
id?: string | null;
|
|
31
31
|
title?: string | null;
|
|
32
32
|
sortOrder?: number | null;
|
|
33
|
+
chapterCount?: number | null;
|
|
34
|
+
filteredChapterCount?: number | null;
|
|
33
35
|
chapters?: T[];
|
|
34
36
|
};
|
|
35
37
|
|
|
36
38
|
export type OutlineBoard<T extends OutlineBoardChapter = OutlineBoardChapter> = {
|
|
37
39
|
volumes?: Array<OutlineBoardVolume<T>>;
|
|
40
|
+
volumeOptions?: Array<Omit<OutlineBoardVolume<T>, "chapters">>;
|
|
41
|
+
page?: number;
|
|
42
|
+
limit?: number;
|
|
43
|
+
itemCount?: number;
|
|
44
|
+
total?: number;
|
|
45
|
+
pageCount?: number;
|
|
46
|
+
hasMore?: boolean;
|
|
47
|
+
nextPage?: number | null;
|
|
38
48
|
};
|
|
39
49
|
|
|
40
50
|
export type OutlineBoardState = {
|
|
@@ -47,15 +57,11 @@ export type OutlineBoardState = {
|
|
|
47
57
|
|
|
48
58
|
export declare function normalizeOutlineBoardState(value?: Partial<OutlineBoardState>): OutlineBoardState;
|
|
49
59
|
|
|
50
|
-
export declare function
|
|
51
|
-
|
|
52
|
-
value?: Partial<OutlineBoardState
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
totalChapterCount: number;
|
|
57
|
-
visibleChapterCount: number;
|
|
58
|
-
filtersActive: boolean;
|
|
59
|
-
};
|
|
60
|
+
export declare function outlineBoardRequestPath(
|
|
61
|
+
workId: string,
|
|
62
|
+
value?: Partial<OutlineBoardState>,
|
|
63
|
+
page?: number,
|
|
64
|
+
limit?: number
|
|
65
|
+
): string;
|
|
60
66
|
|
|
61
67
|
export declare function outlineBoardUnresolvedCount(chapter: OutlineBoardChapter): number;
|