@musnows/scriverse 1.0.10 → 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/ai-interactive.js +21 -1
- package/dist/public/app.js +143 -39
- 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/store.js +6 -14
- package/dist/store.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
|
@@ -186,9 +186,15 @@ export function normalizeAiQuestionItems(question) {
|
|
|
186
186
|
* 会话内的最新审批详情缓存:确认/撤销之后写回,避免卡片在重渲染时回退到提交时刻的快照。
|
|
187
187
|
*/
|
|
188
188
|
const livePlanDetails = new Map();
|
|
189
|
+
const livePlanCardDefinitions = new WeakMap();
|
|
189
190
|
|
|
190
191
|
export function cacheAiWritePlanDetail(detail) {
|
|
191
|
-
if (detail?.id)
|
|
192
|
+
if (!detail?.id) return;
|
|
193
|
+
const planId = String(detail.id);
|
|
194
|
+
const previous = livePlanDetails.get(planId);
|
|
195
|
+
// 审批中心列表只返回摘要,不能覆盖已加载详情中的操作明细。
|
|
196
|
+
livePlanDetails.set(planId, previous ? { ...previous, ...detail } : detail);
|
|
197
|
+
refreshAiWritePlanCards(planId);
|
|
192
198
|
}
|
|
193
199
|
|
|
194
200
|
export function cachedAiWritePlanDetail(planId) {
|
|
@@ -268,6 +274,18 @@ function actionButton(labelText, className, onClick) {
|
|
|
268
274
|
return button;
|
|
269
275
|
}
|
|
270
276
|
|
|
277
|
+
/** 将审批中心或详情接口的最新状态同步到当前已显示的会话卡片。 */
|
|
278
|
+
function refreshAiWritePlanCards(planId) {
|
|
279
|
+
if (typeof document === "undefined") return;
|
|
280
|
+
for (const card of document.querySelectorAll("[data-ai-write-plan-id]")) {
|
|
281
|
+
if (card.dataset.aiWritePlanId !== String(planId)) continue;
|
|
282
|
+
const definition = livePlanCardDefinitions.get(card);
|
|
283
|
+
if (!definition) continue;
|
|
284
|
+
const replacement = buildPlanCard(definition.model, definition.actions);
|
|
285
|
+
card.replaceWith(replacement);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
271
289
|
function buildPlanCard(model, actions) {
|
|
272
290
|
// 优先使用缓存中的最新详情(用户刚刚确认/撤销过),否则回退到工具结果摘要。
|
|
273
291
|
const liveDetail = model.plan ? cachedAiWritePlanDetail(model.plan.id) : null;
|
|
@@ -312,6 +330,7 @@ function buildPlanCard(model, actions) {
|
|
|
312
330
|
const actionsBar = document.createElement("div");
|
|
313
331
|
actionsBar.className = "ai-interactive-actions";
|
|
314
332
|
const planId = model.plan?.id ?? liveDetail?.id ?? "";
|
|
333
|
+
if (planId) card.dataset.aiWritePlanId = String(planId);
|
|
315
334
|
actionsBar.append(actionButton("完整修改明细", "ghost-button ai-card-action", () => actions.openPlanDetail(planId)));
|
|
316
335
|
if (status === "pending") {
|
|
317
336
|
actionsBar.append(
|
|
@@ -329,6 +348,7 @@ function buildPlanCard(model, actions) {
|
|
|
329
348
|
? "确认后系统会重新校验权限、开关与目标版本,全部通过才原子执行;拒绝或过期都不会产生任何写入。"
|
|
330
349
|
: (model.message || "详情与审计记录见 AI 操作审批中心。");
|
|
331
350
|
card.append(note);
|
|
351
|
+
livePlanCardDefinitions.set(card, { model, actions });
|
|
332
352
|
return card;
|
|
333
353
|
}
|
|
334
354
|
|
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,
|
|
@@ -54,7 +61,7 @@ import {
|
|
|
54
61
|
renderWritePlanDetailMarkup,
|
|
55
62
|
isInteractiveToolPending,
|
|
56
63
|
aiFormatDateTime
|
|
57
|
-
} from "/ai-interactive.js?v=
|
|
64
|
+
} from "/ai-interactive.js?v=20260915-plan-status-sync-v1";
|
|
58
65
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
59
66
|
import { bindPlainTextPaste } from "/plain-text-paste.js?v=20260815-plain-text-paste-v1";
|
|
60
67
|
import { clipboardImageFiles } from "/character-markdown.js?v=20260820-ai-chat-image-attachments-v1";
|
|
@@ -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
|
});
|
|
@@ -3605,7 +3628,17 @@ async function loadAiApprovalCenterPlans() {
|
|
|
3605
3628
|
]);
|
|
3606
3629
|
const plans = Array.isArray(planPayload) ? planPayload : (Array.isArray(planPayload?.plans) ? planPayload.plans : []);
|
|
3607
3630
|
const questions = Array.isArray(questionPayload) ? questionPayload : (Array.isArray(questionPayload?.questions) ? questionPayload.questions : []);
|
|
3631
|
+
plans.forEach(cacheAiWritePlanDetail);
|
|
3608
3632
|
$("#ai-approval-list-host").innerHTML = renderApprovalCenterRows(plans, questions);
|
|
3633
|
+
await syncVisibleAiWritePlanCardStatuses();
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
/** 审批中心的筛选结果不一定覆盖当前会话中的旧卡片,逐项读取其权威状态。 */
|
|
3637
|
+
async function syncVisibleAiWritePlanCardStatuses() {
|
|
3638
|
+
const planIds = [...document.querySelectorAll("[data-ai-write-plan-id]")]
|
|
3639
|
+
.map((card) => String(card.dataset.aiWritePlanId ?? ""))
|
|
3640
|
+
.filter(Boolean);
|
|
3641
|
+
await Promise.allSettled([...new Set(planIds)].map((planId) => fetchAiWritePlanDetail(planId)));
|
|
3609
3642
|
}
|
|
3610
3643
|
|
|
3611
3644
|
function openAiApprovalCenter() {
|
|
@@ -6514,7 +6547,7 @@ function invalidateAuthentication() {
|
|
|
6514
6547
|
showAuth(false);
|
|
6515
6548
|
}
|
|
6516
6549
|
|
|
6517
|
-
function showAuth(setupRequired, registrationOpen = false, setupTokenRequired = false) {
|
|
6550
|
+
function showAuth(setupRequired, registrationOpen = false, setupTokenRequired = false, registrationMode = "disabled") {
|
|
6518
6551
|
if (state.user) return;
|
|
6519
6552
|
document.documentElement.classList.remove("dev-auth-bypass");
|
|
6520
6553
|
document.documentElement.classList.add("login-route");
|
|
@@ -6523,14 +6556,17 @@ function showAuth(setupRequired, registrationOpen = false, setupTokenRequired =
|
|
|
6523
6556
|
document.body.classList.add("auth-pending");
|
|
6524
6557
|
$("#auth-view").classList.remove("hidden");
|
|
6525
6558
|
const canRegister = registrationOpen === true;
|
|
6559
|
+
const inviteRequired = registrationMode === "invite" && setupRequired !== true;
|
|
6526
6560
|
$("#auth-title").textContent = setupRequired
|
|
6527
6561
|
? canRegister ? "创建首个管理员账户" : "注册已禁用"
|
|
6528
6562
|
: "登录后继续创作";
|
|
6529
6563
|
$("#auth-description").textContent = setupRequired
|
|
6530
6564
|
? canRegister
|
|
6531
6565
|
? "这是首次启动。首个注册用户会成为系统管理员,并接管现有作品。"
|
|
6532
|
-
: "请将 APP_ALLOW_REGISTRATION 设置为 true 后创建首个管理员账户。"
|
|
6533
|
-
:
|
|
6566
|
+
: "请将 APP_ALLOW_REGISTRATION 设置为 true 或 invite 后创建首个管理员账户。"
|
|
6567
|
+
: inviteRequired
|
|
6568
|
+
? "当前部署需要邀请码才能注册。请向系统管理员获取一次性邀请码。"
|
|
6569
|
+
: "你的作品、协作权限和每一次修改都会绑定到账户。";
|
|
6534
6570
|
const registerTab = $("#auth-register-tab");
|
|
6535
6571
|
registerTab.disabled = !canRegister;
|
|
6536
6572
|
registerTab.setAttribute("aria-disabled", String(!canRegister));
|
|
@@ -6539,12 +6575,19 @@ function showAuth(setupRequired, registrationOpen = false, setupTokenRequired =
|
|
|
6539
6575
|
const setupTokenInput = setupTokenField.querySelector('input[name="setupToken"]');
|
|
6540
6576
|
setupTokenField.classList.toggle("hidden", !setupTokenRequired);
|
|
6541
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;
|
|
6542
6582
|
selectAuthMode(setupRequired && canRegister ? "register" : "login");
|
|
6543
6583
|
}
|
|
6544
6584
|
|
|
6545
6585
|
function applyAuthenticatedUser(session) {
|
|
6546
6586
|
state.user = session.user;
|
|
6547
6587
|
state.csrfToken = session.csrfToken;
|
|
6588
|
+
state.registrationMode = session.registrationMode === "invite" || session.registrationMode === "open"
|
|
6589
|
+
? session.registrationMode
|
|
6590
|
+
: "disabled";
|
|
6548
6591
|
const isSystemAdmin = session.user.isSystemAdmin === true;
|
|
6549
6592
|
const accountButton = $("#account-button");
|
|
6550
6593
|
$("#account-name").textContent = session.user.displayName;
|
|
@@ -6627,7 +6670,7 @@ async function initializeAuthentication() {
|
|
|
6627
6670
|
if (!session.authenticated) {
|
|
6628
6671
|
// 未登录时一律转到登录页路由;登录页本身则保持原样
|
|
6629
6672
|
if (route.view !== "login") window.history.replaceState(null, "", serializePageRoute({ view: "login" }));
|
|
6630
|
-
showAuth(session.setupRequired, session.registrationOpen === true, session.setupTokenRequired === true);
|
|
6673
|
+
showAuth(session.setupRequired, session.registrationOpen === true, session.setupTokenRequired === true, session.registrationMode);
|
|
6631
6674
|
return false;
|
|
6632
6675
|
}
|
|
6633
6676
|
// 已登录却停在登录页路由时,回到书架首页
|
|
@@ -7726,9 +7769,61 @@ async function openUsersDialog() {
|
|
|
7726
7769
|
return;
|
|
7727
7770
|
}
|
|
7728
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");
|
|
7729
7774
|
$("#users-dialog").showModal();
|
|
7730
|
-
try {
|
|
7731
|
-
|
|
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
|
+
}
|
|
7732
7827
|
}
|
|
7733
7828
|
|
|
7734
7829
|
function renderAdminAiConversationFilterOptions(works, users) {
|
|
@@ -20323,6 +20418,7 @@ $("#register-form").addEventListener("submit", async (event) => {
|
|
|
20323
20418
|
password: form.get("password"),
|
|
20324
20419
|
passwordConfirmation: form.get("passwordConfirmation"),
|
|
20325
20420
|
setupToken: form.get("setupToken") || undefined,
|
|
20421
|
+
inviteCode: form.get("inviteCode") || undefined,
|
|
20326
20422
|
captchaId: form.get("captchaId"),
|
|
20327
20423
|
captchaAnswer: form.get("captchaAnswer")
|
|
20328
20424
|
}
|
|
@@ -20365,8 +20461,15 @@ $("#platform-usage-pricing-refresh").addEventListener("click", async () => {
|
|
|
20365
20461
|
}
|
|
20366
20462
|
});
|
|
20367
20463
|
$("#user-management-button").addEventListener("click", openUsersDialog);
|
|
20464
|
+
$("#invite-code-generate").addEventListener("click", () => generateRegistrationInvite().catch((error) => toast(error.message, "error")));
|
|
20368
20465
|
$("#admin-ai-conversations-button").addEventListener("click", () => openAdminAiConversationsDialog().catch((error) => toast(error.message, "error")));
|
|
20369
|
-
$("#writing-progress-button").addEventListener("click", () =>
|
|
20466
|
+
$("#writing-progress-button").addEventListener("click", () => {
|
|
20467
|
+
void openWritingProgressDialog().catch((error) => toast(error.message, "error"));
|
|
20468
|
+
if (isMobileViewport()) {
|
|
20469
|
+
panelLayout.leftCollapsed = true;
|
|
20470
|
+
applyPanelLayout(true);
|
|
20471
|
+
}
|
|
20472
|
+
});
|
|
20370
20473
|
$("#writing-progress-close").addEventListener("click", () => $("#writing-progress-dialog").close());
|
|
20371
20474
|
$("#writing-progress-refresh").addEventListener("click", () => loadWritingProgress().catch((error) => toast(error.message, "error")));
|
|
20372
20475
|
$("#writing-goal-form").addEventListener("submit", saveWritingGoal);
|
|
@@ -20749,6 +20852,7 @@ $("#chapter-content").addEventListener("input", (event) => {
|
|
|
20749
20852
|
});
|
|
20750
20853
|
$("#chapter-content").addEventListener("select", () => setAiContextMeter(null));
|
|
20751
20854
|
$("#chapter-content").addEventListener("scroll", syncChapterLineNumberScroll);
|
|
20855
|
+
$("#chapter-editor-scroll").addEventListener("scroll", syncChapterLineNumberScroll, { passive: true });
|
|
20752
20856
|
$("#chapter-content").addEventListener("contextmenu", (event) => {
|
|
20753
20857
|
if (!state.chapter) return;
|
|
20754
20858
|
showLineCitationMenu(event, lineIndexAtPointer(event.clientY));
|
|
@@ -20840,7 +20944,7 @@ $("#ai-semantic-query").addEventListener("keydown", (event) => {
|
|
|
20840
20944
|
$("#ai-semantic-inject").addEventListener("click", () => { void injectAiSemanticSelection(); });
|
|
20841
20945
|
setupPanelResize($("#left-panel-resize"), "left");
|
|
20842
20946
|
setupPanelResize($("#ai-panel-resize"), "ai");
|
|
20843
|
-
if (typeof ResizeObserver !== "undefined") new ResizeObserver(scheduleChapterLineNumbers).observe($("#chapter-
|
|
20947
|
+
if (typeof ResizeObserver !== "undefined") new ResizeObserver(scheduleChapterLineNumbers).observe($("#chapter-editor-scroll"));
|
|
20844
20948
|
if (typeof ResizeObserver !== "undefined") new ResizeObserver(syncMobileAiPanelSafeTop).observe($("#chapter-foreshadow-reminder"));
|
|
20845
20949
|
window.addEventListener("resize", () => {
|
|
20846
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">
|
|
@@ -144,6 +145,7 @@
|
|
|
144
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>
|
|
145
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>
|
|
146
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>
|
|
147
149
|
</nav>
|
|
148
150
|
<div class="panel-heading">
|
|
149
151
|
<span class="panel-heading-label"><span class="panel-heading-label-full">作品目录</span><span class="panel-heading-label-compact">目录</span></span>
|
|
@@ -239,12 +241,11 @@
|
|
|
239
241
|
<div class="settings-hub-grid">
|
|
240
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>
|
|
241
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>
|
|
242
|
-
<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>
|
|
243
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>
|
|
244
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>
|
|
245
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>
|
|
246
248
|
<button id="collaboration-button" class="settings-hub-card" type="button"><span class="settings-card-mark">协</span><strong>作品协作</strong><small>邀请注册用户共同编辑当前作品</small></button>
|
|
247
|
-
<button id="writing-progress-button" class="settings-hub-card" type="button"><span class="settings-card-mark">字</span><strong>写作目标</strong><small>每日目标、总字数目标与近 30 天趋势</small></button>
|
|
248
249
|
<button id="work-audit-button" class="settings-hub-card" type="button"><span class="settings-card-mark">录</span><strong>操作记录</strong><small>查看作品修改、操作者、对象与时间</small></button>
|
|
249
250
|
<button id="global-replace-button" class="settings-hub-card" type="button"><span class="settings-card-mark">替</span><strong>全局替换</strong><small>替换已保存正文、设定库或两者内容</small></button>
|
|
250
251
|
<button id="appearance-button" class="settings-hub-card" type="button" data-settings-action="appearance"><span class="settings-card-mark">Aa</span><strong>显示设置</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"></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>
|