@musnows/scriverse 1.0.11 → 1.0.12
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/README.en.md +3 -3
- package/README.md +3 -3
- package/dist/app.js +105 -9
- package/dist/app.js.map +1 -1
- package/dist/database.js +29 -2
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +125 -37
- package/dist/public/chapter-editor-scroll.d.ts +37 -0
- package/dist/public/chapter-editor-scroll.js +52 -0
- package/dist/public/index.html +22 -8
- package/dist/public/styles.css +47 -7
- package/dist/security.js +22 -3
- package/dist/security.js.map +1 -1
- package/dist/user-auth.js +125 -15
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -21,6 +21,13 @@ import { aiRequestTargetsState, createAiRequestAbortError, createAiRequestManage
|
|
|
21
21
|
import { calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
|
|
22
22
|
import { buildChapterLineMirror, findChapterLineWindow } from "/chapter-editor-virtualization.js?v=20260810-visible-lines-v1";
|
|
23
23
|
import { CHAPTER_PARAGRAPH_INDENT, calculateChapterCaretScroll, chapterLineIndexAtOffset, insertIndentedParagraph } from "/chapter-editor-behavior.js?v=20260828-centered-scroll-v1";
|
|
24
|
+
import {
|
|
25
|
+
calculateChapterEditorContentHeight,
|
|
26
|
+
chapterLineNumberLayerTransform,
|
|
27
|
+
needsChapterLineVirtualWindowRefresh,
|
|
28
|
+
readChapterEditorScrollMetrics,
|
|
29
|
+
transferNestedEditorScroll
|
|
30
|
+
} from "/chapter-editor-scroll.js?v=20260916-line-scroll-bind-v1";
|
|
24
31
|
import {
|
|
25
32
|
FORESHADOW_REMINDER_SNOOZE_STORAGE_KEY,
|
|
26
33
|
foreshadowReminderRequestTargetsState,
|
|
@@ -202,6 +209,7 @@ function isSelectableModel(model) {
|
|
|
202
209
|
const state = {
|
|
203
210
|
user: null,
|
|
204
211
|
csrfToken: null,
|
|
212
|
+
registrationMode: "disabled",
|
|
205
213
|
works: [],
|
|
206
214
|
work: null,
|
|
207
215
|
chapter: null,
|
|
@@ -501,6 +509,7 @@ function applyWorkAccessMode() {
|
|
|
501
509
|
$("#module-nav [data-module=\"comments\"]").classList.toggle("permission-hidden", Boolean(state.work) && !canReadModule("comments"));
|
|
502
510
|
$("#module-nav [data-work-settings]").classList.toggle("permission-hidden", Boolean(state.work) && !canManageWork());
|
|
503
511
|
$("#reader-open-button").classList.toggle("permission-hidden", proseHidden);
|
|
512
|
+
$("#writing-progress-button").classList.toggle("permission-hidden", Boolean(state.work) && !canReadModule("editor"));
|
|
504
513
|
$("#ai-assistant-entry").classList.toggle("permission-hidden", !state.work || aiHidden);
|
|
505
514
|
$("#new-volume-button").classList.toggle("permission-hidden", Boolean(state.work) && proseReadOnly);
|
|
506
515
|
$("#chapter-batch-button").classList.toggle("permission-hidden", Boolean(state.work) && proseReadOnly);
|
|
@@ -1739,24 +1748,27 @@ function setModuleNavExpanded(expanded) {
|
|
|
1739
1748
|
$("#module-nav").querySelectorAll(".module-nav-secondary").forEach((button) => button.classList.toggle("hidden", !expanded));
|
|
1740
1749
|
}
|
|
1741
1750
|
|
|
1751
|
+
function getChapterEditorScroller() {
|
|
1752
|
+
// 行号与正文只允许这个容器滚动。
|
|
1753
|
+
return $("#chapter-editor-scroll");
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1742
1756
|
function syncChapterLineNumberScroll() {
|
|
1757
|
+
const scroller = getChapterEditorScroller();
|
|
1743
1758
|
const input = $("#chapter-content");
|
|
1744
1759
|
const inner = $("#chapter-line-numbers-inner");
|
|
1745
1760
|
const whitespace = $("#chapter-whitespace-inner");
|
|
1746
|
-
if (!
|
|
1747
|
-
|
|
1748
|
-
|
|
1761
|
+
if (!scroller || !inner) return;
|
|
1762
|
+
if (input) transferNestedEditorScroll(input, scroller);
|
|
1763
|
+
const metrics = readChapterEditorScrollMetrics(scroller);
|
|
1764
|
+
const layerTransform = chapterLineNumberLayerTransform();
|
|
1765
|
+
inner.style.transform = layerTransform;
|
|
1766
|
+
inner.dataset.scrollTop = String(metrics.scrollTop);
|
|
1749
1767
|
if (whitespace) {
|
|
1750
|
-
whitespace.style.transform =
|
|
1751
|
-
whitespace.dataset.scrollTop = String(
|
|
1768
|
+
whitespace.style.transform = layerTransform;
|
|
1769
|
+
whitespace.dataset.scrollTop = String(metrics.scrollTop);
|
|
1752
1770
|
}
|
|
1753
|
-
if (
|
|
1754
|
-
const buffer = input.clientHeight * 0.35;
|
|
1755
|
-
const viewportBottom = input.scrollTop + input.clientHeight;
|
|
1756
|
-
const needsPreviousLines = chapterLineVirtualWindow.start > 0 && input.scrollTop < chapterLineVirtualWindow.top + buffer;
|
|
1757
|
-
const needsNextLines = chapterLineVirtualWindow.end < chapterLineVirtualWindow.lineCount
|
|
1758
|
-
&& viewportBottom > chapterLineVirtualWindow.bottom - buffer;
|
|
1759
|
-
if (needsPreviousLines || needsNextLines) scheduleChapterLineNumbers();
|
|
1771
|
+
if (needsChapterLineVirtualWindowRefresh(chapterLineVirtualWindow, metrics)) scheduleChapterLineNumbers();
|
|
1760
1772
|
}
|
|
1761
1773
|
|
|
1762
1774
|
function syncChapterWhitespaceControls() {
|
|
@@ -1908,7 +1920,8 @@ function renderChapterLineNumbers({ targetLineIndex = null } = {}) {
|
|
|
1908
1920
|
const input = $("#chapter-content");
|
|
1909
1921
|
const inner = $("#chapter-line-numbers-inner");
|
|
1910
1922
|
const measure = $("#chapter-line-measure");
|
|
1911
|
-
|
|
1923
|
+
const scroller = getChapterEditorScroller();
|
|
1924
|
+
if (!input || !inner || !measure || !scroller || input.clientWidth === 0) return;
|
|
1912
1925
|
const style = getComputedStyle(input);
|
|
1913
1926
|
const contentWidth = Math.max(1, input.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight));
|
|
1914
1927
|
const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.55;
|
|
@@ -1919,20 +1932,31 @@ function renderChapterLineNumbers({ targetLineIndex = null } = {}) {
|
|
|
1919
1932
|
const numberTextOffset = calculateLineNumberTextOffset(lineHeight, numberLineHeight);
|
|
1920
1933
|
const layout = prepareChapterLineLayout(input, measure, style, contentWidth);
|
|
1921
1934
|
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
1922
|
-
const
|
|
1923
|
-
const
|
|
1924
|
-
|
|
1935
|
+
const { getLineBounds, measureHeight } = createChapterLineBoundsGetter(layout, measure, lineHeight);
|
|
1936
|
+
const nextHeight = calculateChapterEditorContentHeight({
|
|
1937
|
+
measureHeight,
|
|
1938
|
+
paddingTop,
|
|
1939
|
+
paddingBottom,
|
|
1940
|
+
viewportHeight: scroller.clientHeight
|
|
1941
|
+
});
|
|
1942
|
+
const nextHeightPx = `${nextHeight}px`;
|
|
1943
|
+
if (input.style.height !== nextHeightPx) {
|
|
1944
|
+
const savedScrollTop = scroller.scrollTop;
|
|
1945
|
+
input.style.height = nextHeightPx;
|
|
1946
|
+
scroller.scrollTop = savedScrollTop;
|
|
1947
|
+
}
|
|
1948
|
+
const metrics = readChapterEditorScrollMetrics(scroller);
|
|
1925
1949
|
if (Number.isInteger(targetLineIndex)) {
|
|
1926
1950
|
const safeTarget = Math.max(0, Math.min(targetLineIndex, layout.lines.length - 1));
|
|
1927
|
-
|
|
1951
|
+
scroller.scrollTop = Math.max(0, getLineBounds(safeTarget).top + paddingTop - metrics.clientHeight / 3);
|
|
1928
1952
|
}
|
|
1929
|
-
const viewportTop = Math.max(0,
|
|
1930
|
-
const overscan = Math.max(
|
|
1953
|
+
const viewportTop = Math.max(0, scroller.scrollTop - paddingTop);
|
|
1954
|
+
const overscan = Math.max(metrics.clientHeight, lineHeight * 8);
|
|
1931
1955
|
const lineWindow = findChapterLineWindow(
|
|
1932
1956
|
layout.lines.length,
|
|
1933
1957
|
getLineBounds,
|
|
1934
1958
|
viewportTop - overscan,
|
|
1935
|
-
viewportTop +
|
|
1959
|
+
viewportTop + metrics.clientHeight + overscan
|
|
1936
1960
|
);
|
|
1937
1961
|
const numbers = document.createDocumentFragment();
|
|
1938
1962
|
for (let index = lineWindow.start; index < lineWindow.end; index += 1) {
|
|
@@ -2018,26 +2042,25 @@ function scheduleChapterCaretScroll() {
|
|
|
2018
2042
|
chapterCaretScrollFrame = null;
|
|
2019
2043
|
const input = $("#chapter-content");
|
|
2020
2044
|
const measure = $("#chapter-line-measure");
|
|
2021
|
-
|
|
2045
|
+
const scroller = getChapterEditorScroller();
|
|
2046
|
+
if (!state.work?.editorTypewriterModeEnabled || document.activeElement !== input || !scroller || input.readOnly || input.clientWidth === 0 || scroller.clientHeight === 0) return;
|
|
2022
2047
|
const style = getComputedStyle(input);
|
|
2023
2048
|
const paddingTop = parseFloat(style.paddingTop) || 0;
|
|
2024
|
-
const paddingBottom = parseFloat(style.paddingBottom) || 0;
|
|
2025
2049
|
const contentWidth = Math.max(1, input.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight));
|
|
2026
2050
|
const lineHeight = parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.55;
|
|
2027
2051
|
const layout = prepareChapterLineLayout(input, measure, style, contentWidth);
|
|
2028
|
-
const
|
|
2029
|
-
const targetHeight = input.scrollHeight > input.clientHeight + 1 ? scrollContentHeight : null;
|
|
2030
|
-
const { getLineBounds } = createChapterLineBoundsGetter(layout, measure, lineHeight, targetHeight);
|
|
2052
|
+
const { getLineBounds } = createChapterLineBoundsGetter(layout, measure, lineHeight);
|
|
2031
2053
|
const lineIndex = Math.min(layout.lines.length - 1, chapterLineIndexAtOffset(input.value, input.selectionEnd));
|
|
2032
2054
|
const caretBottom = getLineBounds(lineIndex).bottom + paddingTop;
|
|
2055
|
+
const metrics = readChapterEditorScrollMetrics(scroller);
|
|
2033
2056
|
const nextScrollTop = calculateChapterCaretScroll({
|
|
2034
2057
|
caretBottom,
|
|
2035
|
-
scrollTop:
|
|
2036
|
-
clientHeight:
|
|
2037
|
-
scrollHeight:
|
|
2058
|
+
scrollTop: metrics.scrollTop,
|
|
2059
|
+
clientHeight: metrics.clientHeight,
|
|
2060
|
+
scrollHeight: metrics.scrollHeight
|
|
2038
2061
|
});
|
|
2039
|
-
if (nextScrollTop ===
|
|
2040
|
-
|
|
2062
|
+
if (nextScrollTop === metrics.scrollTop) return;
|
|
2063
|
+
scroller.scrollTop = nextScrollTop;
|
|
2041
2064
|
syncChapterLineNumberScroll();
|
|
2042
2065
|
scheduleChapterLineNumbers();
|
|
2043
2066
|
});
|
|
@@ -6524,7 +6547,7 @@ function invalidateAuthentication() {
|
|
|
6524
6547
|
showAuth(false);
|
|
6525
6548
|
}
|
|
6526
6549
|
|
|
6527
|
-
function showAuth(setupRequired, registrationOpen = false, setupTokenRequired = false) {
|
|
6550
|
+
function showAuth(setupRequired, registrationOpen = false, setupTokenRequired = false, registrationMode = "disabled") {
|
|
6528
6551
|
if (state.user) return;
|
|
6529
6552
|
document.documentElement.classList.remove("dev-auth-bypass");
|
|
6530
6553
|
document.documentElement.classList.add("login-route");
|
|
@@ -6533,14 +6556,17 @@ function showAuth(setupRequired, registrationOpen = false, setupTokenRequired =
|
|
|
6533
6556
|
document.body.classList.add("auth-pending");
|
|
6534
6557
|
$("#auth-view").classList.remove("hidden");
|
|
6535
6558
|
const canRegister = registrationOpen === true;
|
|
6559
|
+
const inviteRequired = registrationMode === "invite" && setupRequired !== true;
|
|
6536
6560
|
$("#auth-title").textContent = setupRequired
|
|
6537
6561
|
? canRegister ? "创建首个管理员账户" : "注册已禁用"
|
|
6538
6562
|
: "登录后继续创作";
|
|
6539
6563
|
$("#auth-description").textContent = setupRequired
|
|
6540
6564
|
? canRegister
|
|
6541
6565
|
? "这是首次启动。首个注册用户会成为系统管理员,并接管现有作品。"
|
|
6542
|
-
: "请将 APP_ALLOW_REGISTRATION 设置为 true 后创建首个管理员账户。"
|
|
6543
|
-
:
|
|
6566
|
+
: "请将 APP_ALLOW_REGISTRATION 设置为 true 或 invite 后创建首个管理员账户。"
|
|
6567
|
+
: inviteRequired
|
|
6568
|
+
? "当前部署需要邀请码才能注册。请向系统管理员获取一次性邀请码。"
|
|
6569
|
+
: "你的作品、协作权限和每一次修改都会绑定到账户。";
|
|
6544
6570
|
const registerTab = $("#auth-register-tab");
|
|
6545
6571
|
registerTab.disabled = !canRegister;
|
|
6546
6572
|
registerTab.setAttribute("aria-disabled", String(!canRegister));
|
|
@@ -6549,12 +6575,19 @@ function showAuth(setupRequired, registrationOpen = false, setupTokenRequired =
|
|
|
6549
6575
|
const setupTokenInput = setupTokenField.querySelector('input[name="setupToken"]');
|
|
6550
6576
|
setupTokenField.classList.toggle("hidden", !setupTokenRequired);
|
|
6551
6577
|
setupTokenInput.required = setupTokenRequired;
|
|
6578
|
+
const inviteField = $("#register-invite-code-field");
|
|
6579
|
+
const inviteInput = inviteField.querySelector('input[name="inviteCode"]');
|
|
6580
|
+
inviteField.classList.toggle("hidden", !inviteRequired);
|
|
6581
|
+
inviteInput.required = inviteRequired;
|
|
6552
6582
|
selectAuthMode(setupRequired && canRegister ? "register" : "login");
|
|
6553
6583
|
}
|
|
6554
6584
|
|
|
6555
6585
|
function applyAuthenticatedUser(session) {
|
|
6556
6586
|
state.user = session.user;
|
|
6557
6587
|
state.csrfToken = session.csrfToken;
|
|
6588
|
+
state.registrationMode = session.registrationMode === "invite" || session.registrationMode === "open"
|
|
6589
|
+
? session.registrationMode
|
|
6590
|
+
: "disabled";
|
|
6558
6591
|
const isSystemAdmin = session.user.isSystemAdmin === true;
|
|
6559
6592
|
const accountButton = $("#account-button");
|
|
6560
6593
|
$("#account-name").textContent = session.user.displayName;
|
|
@@ -6637,7 +6670,7 @@ async function initializeAuthentication() {
|
|
|
6637
6670
|
if (!session.authenticated) {
|
|
6638
6671
|
// 未登录时一律转到登录页路由;登录页本身则保持原样
|
|
6639
6672
|
if (route.view !== "login") window.history.replaceState(null, "", serializePageRoute({ view: "login" }));
|
|
6640
|
-
showAuth(session.setupRequired, session.registrationOpen === true, session.setupTokenRequired === true);
|
|
6673
|
+
showAuth(session.setupRequired, session.registrationOpen === true, session.setupTokenRequired === true, session.registrationMode);
|
|
6641
6674
|
return false;
|
|
6642
6675
|
}
|
|
6643
6676
|
// 已登录却停在登录页路由时,回到书架首页
|
|
@@ -7736,9 +7769,61 @@ async function openUsersDialog() {
|
|
|
7736
7769
|
return;
|
|
7737
7770
|
}
|
|
7738
7771
|
$("#users-list").innerHTML = '<p class="empty-state">正在读取用户……</p>';
|
|
7772
|
+
$("#invite-codes-list").innerHTML = '<p class="empty-state">正在读取邀请码……</p>';
|
|
7773
|
+
$("#invite-code-latest").classList.add("hidden");
|
|
7739
7774
|
$("#users-dialog").showModal();
|
|
7740
|
-
try {
|
|
7741
|
-
|
|
7775
|
+
try {
|
|
7776
|
+
const [usersPage] = await Promise.all([apiPage("/api/users"), loadRegistrationInvites()]);
|
|
7777
|
+
renderUsers(usersPage.items);
|
|
7778
|
+
} catch (error) { $("#users-dialog").close(); toast(error.message, "error"); }
|
|
7779
|
+
}
|
|
7780
|
+
|
|
7781
|
+
function registrationModeLabel(mode) {
|
|
7782
|
+
if (mode === "invite") return "需要邀请码才能注册";
|
|
7783
|
+
if (mode === "open") return "完全开放注册";
|
|
7784
|
+
return "彻底禁止注册";
|
|
7785
|
+
}
|
|
7786
|
+
|
|
7787
|
+
function renderRegistrationInvites(payload) {
|
|
7788
|
+
const mode = payload?.registrationMode === "invite" || payload?.registrationMode === "open"
|
|
7789
|
+
? payload.registrationMode
|
|
7790
|
+
: "disabled";
|
|
7791
|
+
state.registrationMode = mode;
|
|
7792
|
+
$("#invite-codes-mode").textContent = `当前注册策略:${registrationModeLabel(mode)}。邀请码由系统管理员生成,每个只能使用一次。`;
|
|
7793
|
+
const invites = Array.isArray(payload?.items) ? payload.items : [];
|
|
7794
|
+
$("#invite-codes-list").innerHTML = invites.length
|
|
7795
|
+
? invites.map((invite) => {
|
|
7796
|
+
const used = Boolean(invite.usedAt);
|
|
7797
|
+
const usedBy = invite.usedBy ? `${invite.usedBy.displayName} · @${invite.usedBy.username}` : "已失效";
|
|
7798
|
+
return `<article class="access-row invite-code-row">
|
|
7799
|
+
<div class="access-person-copy"><strong>${used ? "已使用" : "未使用"}</strong><small>${esc(formatDateTime(invite.createdAt))} 由 ${esc(invite.createdBy.displayName)} 生成${used ? ` · ${esc(usedBy)} 于 ${esc(formatDateTime(invite.usedAt))}` : ""}</small></div>
|
|
7800
|
+
<span>${used ? "已核销" : "待使用"}</span>
|
|
7801
|
+
</article>`;
|
|
7802
|
+
}).join("")
|
|
7803
|
+
: '<p class="empty-state">还没有邀请码。需要邀请码注册时,请先生成。</p>';
|
|
7804
|
+
}
|
|
7805
|
+
|
|
7806
|
+
async function loadRegistrationInvites() {
|
|
7807
|
+
const payload = await api("/api/registration-invites");
|
|
7808
|
+
renderRegistrationInvites(payload);
|
|
7809
|
+
return payload;
|
|
7810
|
+
}
|
|
7811
|
+
|
|
7812
|
+
async function generateRegistrationInvite() {
|
|
7813
|
+
const button = $("#invite-code-generate");
|
|
7814
|
+
button.disabled = true;
|
|
7815
|
+
try {
|
|
7816
|
+
const created = await api("/api/registration-invites", { method: "POST", body: {} });
|
|
7817
|
+
const latest = $("#invite-code-latest");
|
|
7818
|
+
latest.innerHTML = `新邀请码(请立即复制,页面不会再次完整显示):<strong>${esc(created.code)}</strong>`;
|
|
7819
|
+
latest.classList.remove("hidden");
|
|
7820
|
+
await loadRegistrationInvites();
|
|
7821
|
+
toast("邀请码已生成");
|
|
7822
|
+
} catch (error) {
|
|
7823
|
+
toast(error.message, "error");
|
|
7824
|
+
} finally {
|
|
7825
|
+
button.disabled = false;
|
|
7826
|
+
}
|
|
7742
7827
|
}
|
|
7743
7828
|
|
|
7744
7829
|
function renderAdminAiConversationFilterOptions(works, users) {
|
|
@@ -20333,6 +20418,7 @@ $("#register-form").addEventListener("submit", async (event) => {
|
|
|
20333
20418
|
password: form.get("password"),
|
|
20334
20419
|
passwordConfirmation: form.get("passwordConfirmation"),
|
|
20335
20420
|
setupToken: form.get("setupToken") || undefined,
|
|
20421
|
+
inviteCode: form.get("inviteCode") || undefined,
|
|
20336
20422
|
captchaId: form.get("captchaId"),
|
|
20337
20423
|
captchaAnswer: form.get("captchaAnswer")
|
|
20338
20424
|
}
|
|
@@ -20375,6 +20461,7 @@ $("#platform-usage-pricing-refresh").addEventListener("click", async () => {
|
|
|
20375
20461
|
}
|
|
20376
20462
|
});
|
|
20377
20463
|
$("#user-management-button").addEventListener("click", openUsersDialog);
|
|
20464
|
+
$("#invite-code-generate").addEventListener("click", () => generateRegistrationInvite().catch((error) => toast(error.message, "error")));
|
|
20378
20465
|
$("#admin-ai-conversations-button").addEventListener("click", () => openAdminAiConversationsDialog().catch((error) => toast(error.message, "error")));
|
|
20379
20466
|
$("#writing-progress-button").addEventListener("click", () => {
|
|
20380
20467
|
void openWritingProgressDialog().catch((error) => toast(error.message, "error"));
|
|
@@ -20765,6 +20852,7 @@ $("#chapter-content").addEventListener("input", (event) => {
|
|
|
20765
20852
|
});
|
|
20766
20853
|
$("#chapter-content").addEventListener("select", () => setAiContextMeter(null));
|
|
20767
20854
|
$("#chapter-content").addEventListener("scroll", syncChapterLineNumberScroll);
|
|
20855
|
+
$("#chapter-editor-scroll").addEventListener("scroll", syncChapterLineNumberScroll, { passive: true });
|
|
20768
20856
|
$("#chapter-content").addEventListener("contextmenu", (event) => {
|
|
20769
20857
|
if (!state.chapter) return;
|
|
20770
20858
|
showLineCitationMenu(event, lineIndexAtPointer(event.clientY));
|
|
@@ -20856,7 +20944,7 @@ $("#ai-semantic-query").addEventListener("keydown", (event) => {
|
|
|
20856
20944
|
$("#ai-semantic-inject").addEventListener("click", () => { void injectAiSemanticSelection(); });
|
|
20857
20945
|
setupPanelResize($("#left-panel-resize"), "left");
|
|
20858
20946
|
setupPanelResize($("#ai-panel-resize"), "ai");
|
|
20859
|
-
if (typeof ResizeObserver !== "undefined") new ResizeObserver(scheduleChapterLineNumbers).observe($("#chapter-
|
|
20947
|
+
if (typeof ResizeObserver !== "undefined") new ResizeObserver(scheduleChapterLineNumbers).observe($("#chapter-editor-scroll"));
|
|
20860
20948
|
if (typeof ResizeObserver !== "undefined") new ResizeObserver(syncMobileAiPanelSafeTop).observe($("#chapter-foreshadow-reminder"));
|
|
20861
20949
|
window.addEventListener("resize", () => {
|
|
20862
20950
|
if (isMobileViewport() && $("#onboarding-dialog").open) completeOnboarding();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type ChapterEditorScrollMetrics = {
|
|
2
|
+
scrollTop: number;
|
|
3
|
+
scrollLeft: number;
|
|
4
|
+
clientHeight: number;
|
|
5
|
+
clientWidth: number;
|
|
6
|
+
scrollHeight: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type ChapterLineVirtualWindow = {
|
|
10
|
+
start: number;
|
|
11
|
+
end: number;
|
|
12
|
+
lineCount: number;
|
|
13
|
+
top: number;
|
|
14
|
+
bottom: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function readChapterEditorScrollMetrics(scroller?: { scrollTop?: number; scrollLeft?: number; clientHeight?: number; clientWidth?: number; scrollHeight?: number } | null): ChapterEditorScrollMetrics;
|
|
18
|
+
|
|
19
|
+
export function calculateChapterEditorContentHeight(input?: {
|
|
20
|
+
measureHeight?: number;
|
|
21
|
+
paddingTop?: number;
|
|
22
|
+
paddingBottom?: number;
|
|
23
|
+
viewportHeight?: number;
|
|
24
|
+
}): number;
|
|
25
|
+
|
|
26
|
+
export function needsChapterLineVirtualWindowRefresh(
|
|
27
|
+
virtualWindow?: ChapterLineVirtualWindow | null,
|
|
28
|
+
metrics?: Pick<ChapterEditorScrollMetrics, "scrollTop" | "clientHeight"> | null,
|
|
29
|
+
bufferRatio?: number
|
|
30
|
+
): boolean;
|
|
31
|
+
|
|
32
|
+
export function chapterLineNumberLayerTransform(): "none";
|
|
33
|
+
|
|
34
|
+
export function transferNestedEditorScroll(
|
|
35
|
+
source?: { scrollTop?: number; scrollLeft?: number } | null,
|
|
36
|
+
destination?: { scrollTop?: number; scrollLeft?: number } | null
|
|
37
|
+
): { transferredTop: number; transferredLeft: number };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** 正文行号与文字共用同一滚动容器,行号层不再独立滚动。 */
|
|
2
|
+
|
|
3
|
+
export function readChapterEditorScrollMetrics(scroller) {
|
|
4
|
+
return {
|
|
5
|
+
scrollTop: Math.max(0, Number(scroller?.scrollTop) || 0),
|
|
6
|
+
scrollLeft: Number(scroller?.scrollLeft) || 0,
|
|
7
|
+
clientHeight: Math.max(0, Number(scroller?.clientHeight) || 0),
|
|
8
|
+
clientWidth: Math.max(0, Number(scroller?.clientWidth) || 0),
|
|
9
|
+
scrollHeight: Math.max(0, Number(scroller?.scrollHeight) || 0)
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function calculateChapterEditorContentHeight({
|
|
14
|
+
measureHeight,
|
|
15
|
+
paddingTop,
|
|
16
|
+
paddingBottom,
|
|
17
|
+
viewportHeight
|
|
18
|
+
} = {}) {
|
|
19
|
+
const contentHeight = Math.max(0, Number(measureHeight) || 0)
|
|
20
|
+
+ Math.max(0, Number(paddingTop) || 0)
|
|
21
|
+
+ Math.max(0, Number(paddingBottom) || 0);
|
|
22
|
+
return Math.max(contentHeight, Math.max(0, Number(viewportHeight) || 0));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function needsChapterLineVirtualWindowRefresh(virtualWindow, metrics, bufferRatio = 0.35) {
|
|
26
|
+
if (!virtualWindow) return false;
|
|
27
|
+
const scrollTop = Math.max(0, Number(metrics?.scrollTop) || 0);
|
|
28
|
+
const clientHeight = Math.max(0, Number(metrics?.clientHeight) || 0);
|
|
29
|
+
const buffer = clientHeight * (Number.isFinite(Number(bufferRatio)) ? Number(bufferRatio) : 0.35);
|
|
30
|
+
const viewportBottom = scrollTop + clientHeight;
|
|
31
|
+
const needsPreviousLines = virtualWindow.start > 0 && scrollTop < virtualWindow.top + buffer;
|
|
32
|
+
const needsNextLines = virtualWindow.end < virtualWindow.lineCount
|
|
33
|
+
&& viewportBottom > virtualWindow.bottom - buffer;
|
|
34
|
+
return needsPreviousLines || needsNextLines;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function chapterLineNumberLayerTransform() {
|
|
38
|
+
return "none";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function transferNestedEditorScroll(source, destination) {
|
|
42
|
+
const transferredTop = Number(source?.scrollTop) || 0;
|
|
43
|
+
const transferredLeft = Number(source?.scrollLeft) || 0;
|
|
44
|
+
if (!source || !destination || (transferredTop === 0 && transferredLeft === 0)) {
|
|
45
|
+
return { transferredTop: 0, transferredLeft: 0 };
|
|
46
|
+
}
|
|
47
|
+
destination.scrollTop += transferredTop;
|
|
48
|
+
destination.scrollLeft += transferredLeft;
|
|
49
|
+
source.scrollTop = 0;
|
|
50
|
+
source.scrollLeft = 0;
|
|
51
|
+
return { transferredTop, transferredLeft };
|
|
52
|
+
}
|
package/dist/public/index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<script src="/theme-init.js?v=20260827-reader-prefetch-v1"></script>
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
|
-
<link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=ai-composer-compact-controls-v1&feature=ai-context-meter-ring-only-v2&feature=ai-context-cache-hit-v1&feature=annotation-line-counts-v1&feature=chapter-comment-filters-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=ai-stream-character-count-five-digit-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1&feature=editor-toolbar-compact-v2&feature=reader-first-frame-v1&feature=auth-compact-v2&feature=editor-preview-toggle-v2&feature=setting-title-chrome-v2&feature=entity-pin-icon-center-v1&feature=chapter-center-bottom-space-v1&feature=work-editor-preferences-v1&feature=work-editor-preference-checkbox-v1&feature=ai-roleplay-memory-v2&feature=ai-roleplay-memory-v3&feature=ai-roleplay-memory-v5&feature=character-editor-header-actions-v1&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=roleplay-memory-pin-border-v1&feature=ai-write-tools-v2&feature=ai-write-card-actions-compact-v1&feature=ai-write-plan-actions-footer-v1&feature=ai-question-actions-footer-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=ai-question-batch-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v1&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=ai-settings-textarea-font-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=ai-skill-slash-menu-v1&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=im-narration-contrast-v1&feature=im-member-add-plus-v2&feature=im-button-hierarchy-v1&feature=im-icon-button-size-v1&feature=compact-sidebar-directory-v5&feature=ai-model-config-dialog-v1&feature=system-prompt-override-v3&feature=entity-editor-back-icon-v1&feature=toast-stack-v1&feature=chapter-header-compact-v4&feature=ai-attachment-disabled-v1&feature=ai-prose-acceptance-removed-v1">
|
|
12
|
+
<link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=ai-composer-compact-controls-v1&feature=ai-context-meter-ring-only-v2&feature=ai-context-cache-hit-v1&feature=annotation-line-counts-v1&feature=chapter-comment-filters-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=ai-stream-character-count-five-digit-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1&feature=editor-toolbar-compact-v2&feature=reader-first-frame-v1&feature=auth-compact-v2&feature=editor-preview-toggle-v2&feature=setting-title-chrome-v2&feature=entity-pin-icon-center-v1&feature=chapter-center-bottom-space-v1&feature=work-editor-preferences-v1&feature=work-editor-preference-checkbox-v1&feature=ai-roleplay-memory-v2&feature=ai-roleplay-memory-v3&feature=ai-roleplay-memory-v5&feature=character-editor-header-actions-v1&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=roleplay-memory-pin-border-v1&feature=ai-write-tools-v2&feature=ai-write-card-actions-compact-v1&feature=ai-write-plan-actions-footer-v1&feature=ai-question-actions-footer-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=ai-question-batch-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v1&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=ai-settings-textarea-font-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=ai-skill-slash-menu-v1&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=im-narration-contrast-v1&feature=im-member-add-plus-v2&feature=im-button-hierarchy-v1&feature=im-icon-button-size-v1&feature=compact-sidebar-directory-v5&feature=ai-model-config-dialog-v1&feature=system-prompt-override-v3&feature=entity-editor-back-icon-v1&feature=toast-stack-v1&feature=chapter-header-compact-v4&feature=ai-attachment-disabled-v1&feature=ai-prose-acceptance-removed-v1&feature=chapter-line-scroll-bind-v1&feature=writing-goal-module-navigation-v2&feature=invite-registration-v1">
|
|
13
13
|
</head>
|
|
14
14
|
<body class="auth-pending">
|
|
15
15
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
<form id="register-form" class="auth-form hidden" method="post" action="/api/auth/register">
|
|
41
41
|
<label>用户名<input name="username" autocomplete="username" minlength="3" maxlength="40" pattern="[A-Za-z0-9_.\-\u4e00-\u9fff]+" required></label>
|
|
42
42
|
<label id="register-setup-token-field" class="hidden">初始化令牌<input name="setupToken" type="password" autocomplete="off" minlength="32" maxlength="500" aria-describedby="register-setup-token-hint"><small id="register-setup-token-hint">请输入服务端 APP_SETUP_TOKEN 的值。</small></label>
|
|
43
|
+
<label id="register-invite-code-field" class="hidden">邀请码<input name="inviteCode" type="text" autocomplete="off" maxlength="32" spellcheck="false" autocapitalize="characters" aria-describedby="register-invite-code-hint"><small id="register-invite-code-hint">请输入系统管理员提供的一次性邀请码。</small></label>
|
|
43
44
|
<label>密码<span class="password-field"><input id="register-password" name="password" type="password" autocomplete="new-password" minlength="10" maxlength="200" required><button class="password-toggle" type="button" data-password-toggle="register-password" aria-label="显示密码" aria-pressed="false" title="显示密码"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M2.5 12s3.3-5.5 9.5-5.5S21.5 12 21.5 12 18.2 17.5 12 17.5 2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="2.8"/></svg></button></span></label>
|
|
44
45
|
<label>确认密码<span class="password-field"><input id="register-password-confirmation" name="passwordConfirmation" type="password" autocomplete="new-password" minlength="10" maxlength="200" required><button class="password-toggle" type="button" data-password-toggle="register-password-confirmation" aria-label="显示确认密码" aria-pressed="false" title="显示确认密码"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M2.5 12s3.3-5.5 9.5-5.5S21.5 12 21.5 12 18.2 17.5 12 17.5 2.5 12 2.5 12Z"/><circle cx="12" cy="12" r="2.8"/></svg></button></span></label>
|
|
45
46
|
<div class="auth-captcha">
|
|
@@ -136,7 +137,6 @@
|
|
|
136
137
|
<button type="button" data-module="relationships"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.59 13.51 6.83 3.98"/><path d="m15.41 6.51-6.82 3.98"/></svg>关系</button>
|
|
137
138
|
<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>
|
|
138
139
|
<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>
|
|
139
|
-
<button id="writing-progress-button" type="button"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2"/></svg>写作目标</button>
|
|
140
140
|
<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>
|
|
141
141
|
<button id="ai-assistant-entry" class="module-nav-secondary hidden ai-assistant-entry" type="button" aria-controls="ai-panel" aria-expanded="false"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5.25a3.25 3.25 0 0 0-5.95 1.82A3.74 3.74 0 0 0 4.7 13.5a3.25 3.25 0 0 0 4.55 4.53A3.6 3.6 0 0 0 12 19.5"></path><path d="M12 5.25a3.25 3.25 0 0 1 5.95 1.82 3.74 3.74 0 0 1 1.35 6.43 3.25 3.25 0 0 1-4.55 4.53A3.6 3.6 0 0 1 12 19.5"></path><path d="M12 5.25v14.25"></path><path d="M8 9.1c1.1.1 2 .9 2.25 1.95"></path><path d="M16 9.1c-1.1.1-2 .9-2.25 1.95"></path></svg>创作助手</button>
|
|
142
142
|
<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>
|
|
@@ -145,6 +145,7 @@
|
|
|
145
145
|
<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>
|
|
146
146
|
<button class="module-nav-secondary hidden" type="button" data-module="ai-settings"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><rect width="16" height="16" x="4" y="4" rx="2"/><rect width="6" height="6" x="9" y="9" rx="1"/><path d="M15 2v2"/><path d="M15 20v2"/><path d="M2 15h2"/><path d="M2 9h2"/><path d="M20 15h2"/><path d="M20 9h2"/><path d="M9 2v2"/><path d="M9 20v2"/></svg>AI 设置</button>
|
|
147
147
|
<button class="module-nav-secondary hidden" type="button" data-work-settings><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.09a2 2 0 0 1 1 1.74v.5a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.38a2 2 0 0 0-.73-2.73l-.15-.09a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2Z"/><circle cx="12" cy="12" r="3"/></svg>作品设置</button>
|
|
148
|
+
<button id="writing-progress-button" class="module-nav-secondary hidden" type="button"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2"/></svg><span class="nav-label">写作目标</span></button>
|
|
148
149
|
</nav>
|
|
149
150
|
<div class="panel-heading">
|
|
150
151
|
<span class="panel-heading-label"><span class="panel-heading-label-full">作品目录</span><span class="panel-heading-label-compact">目录</span></span>
|
|
@@ -240,7 +241,7 @@
|
|
|
240
241
|
<div class="settings-hub-grid">
|
|
241
242
|
<button id="platform-ai-button" class="settings-hub-card hidden" type="button" data-settings-action="ai"><span class="settings-card-mark">AI</span><strong>AI 管理</strong><small>供应商、模型、上下文与全局提示词</small></button>
|
|
242
243
|
<button id="platform-usage-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">T</span><strong>Token 用量</strong><small>总消耗、缓存命中率、每日网格与作品明细</small></button>
|
|
243
|
-
<button id="user-management-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">人</span><strong>用户管理</strong><small
|
|
244
|
+
<button id="user-management-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">人</span><strong>用户管理</strong><small>管理员、普通用户、邀请码与账户状态</small></button>
|
|
244
245
|
<button id="admin-ai-conversations-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">话</span><strong>Chat 对话</strong><small>查看所有作品、所有用户的对话记录</small></button>
|
|
245
246
|
<button id="platform-ui-settings-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">界</span><strong>界面与分页</strong><small>设置通知位置、银河图性能及各模块的单页数量</small></button>
|
|
246
247
|
<button id="s3-backup-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">S3</span><strong>S3 备份</strong><small>将整个系统的数据库和图片同步到多个目标</small></button>
|
|
@@ -324,10 +325,12 @@
|
|
|
324
325
|
</dl>
|
|
325
326
|
</section>
|
|
326
327
|
<div class="editor-body">
|
|
327
|
-
<div class="chapter-editor-frame">
|
|
328
|
-
<
|
|
329
|
-
|
|
330
|
-
|
|
328
|
+
<div id="chapter-editor-frame" class="chapter-editor-frame">
|
|
329
|
+
<div id="chapter-editor-scroll" class="chapter-editor-scroll">
|
|
330
|
+
<aside id="chapter-line-numbers" class="chapter-line-numbers" aria-label="正文行号,可拖拽引用多行"><div id="chapter-line-numbers-inner"></div></aside>
|
|
331
|
+
<textarea id="chapter-content" class="chapter-content" aria-label="章节正文" placeholder="在这里开始你的故事……"></textarea>
|
|
332
|
+
<div id="chapter-whitespace-overlay" class="chapter-whitespace-overlay is-visible" aria-hidden="true"><div id="chapter-whitespace-inner"></div></div>
|
|
333
|
+
</div>
|
|
331
334
|
<div id="chapter-line-measure" class="chapter-line-measure" aria-hidden="true"></div>
|
|
332
335
|
</div>
|
|
333
336
|
</div>
|
|
@@ -986,6 +989,17 @@
|
|
|
986
989
|
<dialog id="users-dialog" class="dialog wide-dialog" aria-labelledby="users-dialog-title">
|
|
987
990
|
<div class="dialog-header"><div><span class="eyebrow">系统管理员</span><h2 id="users-dialog-title">用户管理</h2></div><div class="settings-dialog-header-actions"><button id="users-settings-return" class="ghost-button settings-parent-button" type="button">返回设置</button><button id="users-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button></div></div>
|
|
988
991
|
<div class="access-dialog-body">
|
|
992
|
+
<section id="invite-codes-panel" class="invite-codes-panel" aria-labelledby="invite-codes-title">
|
|
993
|
+
<div class="invite-codes-heading">
|
|
994
|
+
<div>
|
|
995
|
+
<h3 id="invite-codes-title">注册邀请码</h3>
|
|
996
|
+
<p id="invite-codes-mode" class="dialog-header-meta">正在读取注册策略……</p>
|
|
997
|
+
</div>
|
|
998
|
+
<button id="invite-code-generate" class="primary-button" type="button">生成邀请码</button>
|
|
999
|
+
</div>
|
|
1000
|
+
<p id="invite-code-latest" class="invite-code-latest hidden" role="status"></p>
|
|
1001
|
+
<div id="invite-codes-list" class="access-list" aria-live="polite"></div>
|
|
1002
|
+
</section>
|
|
989
1003
|
<div id="users-list" class="access-list"></div>
|
|
990
1004
|
</div>
|
|
991
1005
|
</dialog>
|
|
@@ -1427,7 +1441,7 @@
|
|
|
1427
1441
|
</dialog>
|
|
1428
1442
|
|
|
1429
1443
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
1430
|
-
<script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-history-rename-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v2&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1&feature=ai-context-input-output-v1&feature=editor-blank-lines-preserved-v1&feature=reader-first-frame-v1&feature=vditor-lazy-load-v1&feature=ui-module-preload-v1&feature=reader-initial-prefetch-v1&feature=editor-preview-toggle-v2&feature=vditor-fullscreen-disabled-v1&feature=chapter-auto-indent-v1&feature=chapter-centered-scroll-v1&feature=work-editor-preferences-v1&feature=ai-context-output-usage-v1&feature=ai-context-meter-ring-only-v1&feature=ai-context-cache-hit-v1&feature=ai-roleplay-memory-v4&feature=ai-roleplay-memory-v5&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=annotation-line-anchor-v1&feature=stable-line-ids-v1&feature=live-annotation-anchors-v1&feature=chapter-comment-filters-v1&feature=ai-write-tools-v3&feature=ai-question-option-supplement-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-continuation-ui-v3&feature=ai-background-stream-v1&feature=ai-question-tool-result-v1&feature=ai-question-tool-summary-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-batch-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v3&feature=ai-cancel-preserve-process-v2&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=chapter-batch-editor-refresh-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=semantic-search-rag-label-v1&feature=ai-skill-slash-menu-v1&feature=ai-roleplay-scene-bubble-v1&feature=ai-roleplay-scene-collapse-v1&feature=markdown-adjacent-blockquotes-v1&feature=chapter-save-shortcut-v2&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=ai-fork-progress-toast-v1&feature=im-settings-gear-v1&feature=compact-sidebar-directory-v2&feature=chapter-word-count-consistency-v1&feature=ai-all-message-references-v2&feature=ai-question-render-recovery-v3&feature=setting-category-preservation-v1&feature=toast-stack-v1&feature=toast-stack-label-v2&feature=agent-tool-limits-300-v1&feature=server-dev-logo-v1&feature=chapter-directory-ai-reference-v3&feature=ai-error-origin-v1&feature=ai-stream-render-performance-v2&feature=ai-attachment-disabled-v1&feature=ai-prose-acceptance-removed-v1&feature=ai-inline-message-references-v1&feature=ai-write-plan-status-sync-v1&feature=writing-goal-module-navigation-v1"></script>
|
|
1444
|
+
<script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-history-rename-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v2&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1&feature=ai-context-input-output-v1&feature=editor-blank-lines-preserved-v1&feature=reader-first-frame-v1&feature=vditor-lazy-load-v1&feature=ui-module-preload-v1&feature=reader-initial-prefetch-v1&feature=editor-preview-toggle-v2&feature=vditor-fullscreen-disabled-v1&feature=chapter-auto-indent-v1&feature=chapter-centered-scroll-v1&feature=work-editor-preferences-v1&feature=ai-context-output-usage-v1&feature=ai-context-meter-ring-only-v1&feature=ai-context-cache-hit-v1&feature=ai-roleplay-memory-v4&feature=ai-roleplay-memory-v5&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=annotation-line-anchor-v1&feature=stable-line-ids-v1&feature=live-annotation-anchors-v1&feature=chapter-comment-filters-v1&feature=ai-write-tools-v3&feature=ai-question-option-supplement-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-continuation-ui-v3&feature=ai-background-stream-v1&feature=ai-question-tool-result-v1&feature=ai-question-tool-summary-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-batch-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v3&feature=ai-cancel-preserve-process-v2&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=chapter-batch-editor-refresh-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=semantic-search-rag-label-v1&feature=ai-skill-slash-menu-v1&feature=ai-roleplay-scene-bubble-v1&feature=ai-roleplay-scene-collapse-v1&feature=markdown-adjacent-blockquotes-v1&feature=chapter-save-shortcut-v2&feature=ai-message-citation-popover-v1&feature=line-citation-menu-separator-v1&feature=global-im-v106&feature=im-sidebar-compact-v1&feature=ai-fork-progress-toast-v1&feature=im-settings-gear-v1&feature=compact-sidebar-directory-v2&feature=chapter-word-count-consistency-v1&feature=ai-all-message-references-v2&feature=ai-question-render-recovery-v3&feature=setting-category-preservation-v1&feature=toast-stack-v1&feature=toast-stack-label-v2&feature=agent-tool-limits-300-v1&feature=server-dev-logo-v1&feature=chapter-directory-ai-reference-v3&feature=ai-error-origin-v1&feature=ai-stream-render-performance-v2&feature=ai-attachment-disabled-v1&feature=ai-prose-acceptance-removed-v1&feature=ai-inline-message-references-v1&feature=ai-write-plan-status-sync-v1&feature=writing-goal-module-navigation-v2&feature=chapter-line-scroll-bind-v1&feature=invite-registration-v1"></script>
|
|
1431
1445
|
|
|
1432
1446
|
</body>
|
|
1433
1447
|
</html>
|