@musnows/scriverse 1.1.4 → 1.1.5

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.
@@ -2,8 +2,16 @@ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import { AppError } from "./errors.js";
3
3
  const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
4
4
  const captchaLength = 4;
5
- const captchaLifetimeMs = 5 * 60_000;
5
+ /** 默认有效期 300 秒。任何配置或调用若短于下限,都会被抬到 180 秒。 */
6
+ export const CAPTCHA_LIFETIME_DEFAULT_MS = 300_000;
7
+ export const CAPTCHA_LIFETIME_MINIMUM_MS = 180_000;
6
8
  const maximumChallenges = 5_000;
9
+ /** 把请求的有效期抬到下限;未提供或非法时使用默认 300 秒。 */
10
+ export function resolveCaptchaLifetimeMs(requestedMs) {
11
+ if (typeof requestedMs !== "number" || !Number.isFinite(requestedMs))
12
+ return CAPTCHA_LIFETIME_DEFAULT_MS;
13
+ return Math.max(CAPTCHA_LIFETIME_MINIMUM_MS, Math.trunc(requestedMs));
14
+ }
7
15
  /**
8
16
  * 5×7 点阵字形会被渲染为 path,而不是 SVG text:这样不会把答案以可直接读取的
9
17
  * 文本节点放进 data URL。它不是对抗专业视觉模型的单独安全边界,但能阻止最基础的
@@ -44,8 +52,15 @@ const glyphPatterns = {
44
52
  "9": ["01110", "10001", "10001", "01111", "00001", "00001", "01110"]
45
53
  };
46
54
  const fallbackGlyph = ["11111", "00001", "00010", "00100", "00100", "00000", "00100"];
55
+ /**
56
+ * 全角字母和数字与半角外观相同,但直接比较会失败。
57
+ * 先做 NFKC,再去掉空白和格式字符(含零宽空格),最后按 en-US 大写。
58
+ */
59
+ export function normalizeCaptchaAnswer(answer) {
60
+ return answer.normalize("NFKC").replace(/[\p{Cf}\p{Z}\s]+/gu, "").toLocaleUpperCase("en-US");
61
+ }
47
62
  function digestAnswer(answer) {
48
- return createHash("sha256").update(answer.toLocaleUpperCase("en-US")).digest();
63
+ return createHash("sha256").update(normalizeCaptchaAnswer(answer)).digest();
49
64
  }
50
65
  function randomCode() {
51
66
  const bytes = randomBytes(captchaLength);
@@ -131,8 +146,13 @@ export function renderCaptchaSvg(code, seed = randomBytes(8)) {
131
146
  export class ImageCaptchaService {
132
147
  options;
133
148
  challenges = new Map();
149
+ lifetimeMs;
134
150
  constructor(options = {}) {
135
151
  this.options = options;
152
+ this.lifetimeMs = resolveCaptchaLifetimeMs(options.lifetimeMs);
153
+ }
154
+ currentTime() {
155
+ return this.options.now?.() ?? Date.now();
136
156
  }
137
157
  create() {
138
158
  this.pruneExpired();
@@ -145,7 +165,7 @@ export class ImageCaptchaService {
145
165
  const svg = renderCaptchaSvg(answer, seed);
146
166
  this.challenges.set(captchaId, {
147
167
  answerDigest: digestAnswer(answer),
148
- expiresAt: Date.now() + captchaLifetimeMs
168
+ expiresAt: this.currentTime() + this.lifetimeMs
149
169
  });
150
170
  return {
151
171
  captchaId,
@@ -153,20 +173,26 @@ export class ImageCaptchaService {
153
173
  ...(this.options.revealAnswer ? { answer } : {})
154
174
  };
155
175
  }
156
- /** 校验并消费验证码;成功或失败都会使该挑战失效。 */
176
+ /**
177
+ * 校验验证码。答案错误时保留挑战,允许用同一张图继续重试。
178
+ * 只有答案正确后才核销。过期,以及已核销或未知的挑战,返回已失效。
179
+ */
157
180
  consume(captchaId, answer) {
181
+ const currentTime = this.currentTime();
158
182
  const challenge = this.challenges.get(captchaId);
159
- this.challenges.delete(captchaId);
160
- if (!challenge || challenge.expiresAt <= Date.now()) {
183
+ if (!challenge || challenge.expiresAt <= currentTime) {
184
+ if (challenge)
185
+ this.challenges.delete(captchaId);
161
186
  throw new AppError(400, "CAPTCHA_INVALID", "验证码已失效,请刷新后重试");
162
187
  }
163
- const provided = digestAnswer(answer.trim());
188
+ const provided = digestAnswer(answer);
164
189
  if (provided.length !== challenge.answerDigest.length || !timingSafeEqual(provided, challenge.answerDigest)) {
165
- throw new AppError(400, "CAPTCHA_INVALID", "验证码不正确");
190
+ throw new AppError(400, "CAPTCHA_INCORRECT", "验证码不正确");
166
191
  }
192
+ this.challenges.delete(captchaId);
167
193
  }
168
194
  pruneExpired() {
169
- const currentTime = Date.now();
195
+ const currentTime = this.currentTime();
170
196
  for (const [captchaId, challenge] of this.challenges) {
171
197
  if (challenge.expiresAt <= currentTime)
172
198
  this.challenges.delete(captchaId);
@@ -1 +1 @@
1
- {"version":3,"file":"image-captcha.js","sourceRoot":"","sources":["../src/image-captcha.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAcvC,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AACpD,MAAM,aAAa,GAAG,CAAC,CAAC;AACxB,MAAM,iBAAiB,GAAG,CAAC,GAAG,MAAM,CAAC;AACrC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEhC;;;;GAIG;AACH,MAAM,aAAa,GAAsC;IACvD,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,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"}
1
+ {"version":3,"file":"image-captcha.js","sourceRoot":"","sources":["../src/image-captcha.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAcvC,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AACpD,MAAM,aAAa,GAAG,CAAC,CAAC;AACxB,4CAA4C;AAC5C,MAAM,CAAC,MAAM,2BAA2B,GAAG,OAAO,CAAC;AACnD,MAAM,CAAC,MAAM,2BAA2B,GAAG,OAAO,CAAC;AACnD,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEhC,qCAAqC;AACrC,MAAM,UAAU,wBAAwB,CAAC,WAAoB;IAC3D,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,2BAA2B,CAAC;IACzG,OAAO,IAAI,CAAC,GAAG,CAAC,2BAA2B,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;AACxE,CAAC;AAED;;;;GAIG;AACH,MAAM,aAAa,GAAsC;IACvD,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC;IAClE,CAAC,EAAE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"}
@@ -1977,6 +1977,8 @@ let chapterDraftLineIdState = null;
1977
1977
  let chapterBeforeInputState = null;
1978
1978
  let chapterSearchMatchIndex = -1;
1979
1979
  let chapterSelectionRequestId = 0;
1980
+ let chapterBodyAbort = null;
1981
+ let chapterContentReadyId = null;
1980
1982
  let chapterForeshadowReminderRequestId = 0;
1981
1983
  let chapterForeshadowReminders = [];
1982
1984
  let chapterForeshadowReminderIndex = 0;
@@ -2082,9 +2084,12 @@ function applyChapterEditorMode() {
2082
2084
  $("#chapter-content").readOnly = viewOnly;
2083
2085
  $("#chapter-title").setAttribute("aria-readonly", String(viewOnly));
2084
2086
  $("#chapter-content").setAttribute("aria-readonly", String(viewOnly));
2087
+ const waitingForChapter = chapterEditingLocked();
2085
2088
  editButton.classList.toggle("hidden", permissionBlocked || !state.chapter);
2086
2089
  editButton.classList.toggle("primary-button", chapterEditorReadOnly);
2087
2090
  editButton.classList.toggle("ghost-button", !chapterEditorReadOnly);
2091
+ editButton.disabled = waitingForChapter;
2092
+ editButton.setAttribute("aria-disabled", String(waitingForChapter));
2088
2093
  editButton.textContent = chapterEditorReadOnly ? "编辑" : "预览";
2089
2094
  editButton.setAttribute("aria-pressed", String(!chapterEditorReadOnly));
2090
2095
  editButton.setAttribute("aria-label", chapterEditorReadOnly ? "切换到编辑模式" : "切换到预览模式");
@@ -2095,6 +2100,8 @@ function applyChapterEditorMode() {
2095
2100
  }
2096
2101
 
2097
2102
  function enterChapterEditMode() {
2103
+ // 缓存或占位正文还没被本次请求确认前,编辑按钮不进入编辑。
2104
+ if (chapterEditingLocked()) return;
2098
2105
  if (!state.chapter || !canEditProse()) return;
2099
2106
  chapterEditorReadOnly = false;
2100
2107
  applyChapterEditorMode();
@@ -2105,6 +2112,7 @@ function enterChapterEditMode() {
2105
2112
  }
2106
2113
 
2107
2114
  function toggleChapterEditPreviewMode() {
2115
+ if (chapterEditingLocked()) return;
2108
2116
  if (!state.chapter || !canEditProse()) return;
2109
2117
  if (chapterEditorReadOnly) {
2110
2118
  enterChapterEditMode();
@@ -6535,6 +6543,7 @@ async function api(path, options = {}) {
6535
6543
  body: body && typeof body !== "string" ? JSON.stringify(body) : body
6536
6544
  });
6537
6545
  } catch (error) {
6546
+ if (error?.name === "AbortError") throw error;
6538
6547
  if (mobileOfflineRuntime.enabled) {
6539
6548
  const offlineSession = await mobileOfflineRuntime.activateOffline();
6540
6549
  if (offlineSession) return mobileOfflineRuntime.request(path, { method, body });
@@ -7660,6 +7669,7 @@ async function persistChapter({ automatic = false } = {}) {
7660
7669
  const saved = await request;
7661
7670
  if (state.work?.id !== workId || state.chapter?.id !== draft.chapterId) return saved.chapter;
7662
7671
  state.chapter = saved.chapter;
7672
+ chapterContentReadyId = saved.chapter.id;
7663
7673
  state.work = saved.work;
7664
7674
  resetChapterDraftLineIds(state.chapter);
7665
7675
  lastSavedChapterSnapshot = draft;
@@ -9374,6 +9384,9 @@ function renderShelf() {
9374
9384
 
9375
9385
  function resetWorkScopedUiCaches() {
9376
9386
  chapterSelectionRequestId += 1;
9387
+ chapterContentReadyId = null;
9388
+ chapterBodyAbort?.abort();
9389
+ chapterBodyAbort = null;
9377
9390
  clearChapterForeshadowReminders({ invalidateRequest: true });
9378
9391
  invalidateAiConversationNavigation("已切换作品");
9379
9392
  stopBackgroundTaskCenter();
@@ -9861,7 +9874,10 @@ async function submitChapterBatch(event) {
9861
9874
  state.work = await api(`/api/works/${encodeURIComponent(workId)}`);
9862
9875
  const currentEditorVisible = state.module === "editor";
9863
9876
  const currentStillExists = state.chapter && state.work.volumes.some((volume) => volume.chapters.some((chapter) => chapter.id === state.chapter.id));
9864
- if (state.chapter && currentStillExists) state.chapter = await api(`/api/chapters/${encodeURIComponent(state.chapter.id)}`);
9877
+ if (state.chapter && currentStillExists) {
9878
+ state.chapter = await api(`/api/chapters/${encodeURIComponent(state.chapter.id)}`);
9879
+ chapterContentReadyId = state.chapter.id;
9880
+ }
9865
9881
  if (state.chapter && !currentStillExists) {
9866
9882
  state.chapter = null;
9867
9883
  showWelcome(true);
@@ -9997,6 +10013,7 @@ async function deleteChapter(chapterId) {
9997
10013
  const deletingSelectedChapter = state.chapter?.id === chapterId;
9998
10014
  const expectedVersionNo = deletingSelectedChapter ? state.chapter.versionNo : chapter.versionNo;
9999
10015
  await api(`/api/chapters/${chapterId}`, { method: "DELETE", body: { expectedVersionNo } });
10016
+ if (String(chapterContentReadyId ?? "") === String(chapterId)) chapterContentReadyId = null;
10000
10017
  const workId = state.work.id;
10001
10018
  state.work = await api(`/api/works/${workId}`);
10002
10019
  if (deletingSelectedChapter) {
@@ -10215,33 +10232,47 @@ async function resolveCurrentChapterForeshadowReminder() {
10215
10232
  }
10216
10233
  }
10217
10234
 
10218
- async function selectChapter(chapterId, { editMode = false } = {}) {
10219
- imWorkspace.close();
10220
- const workId = state.work?.id;
10221
- if (!workId) return false;
10222
- const selectionGeneration = ++chapterSelectionRequestGeneration;
10223
- const selectionRequestId = ++chapterSelectionRequestId;
10224
- clearChapterForeshadowReminders({ invalidateRequest: true });
10225
- if (state.chapter?.id !== chapterId && !(await confirmDiscardChanges("当前章节有未保存修改,仍要切换吗?"))) {
10226
- if (selectionRequestId === chapterSelectionRequestId) void loadChapterForeshadowReminders();
10227
- return false;
10235
+ function setChapterContentSkeleton(visible) {
10236
+ const skeleton = $("#chapter-content-skeleton");
10237
+ skeleton.classList.toggle("hidden", !visible);
10238
+ skeleton.setAttribute("aria-busy", String(visible));
10239
+ $("#editor-view").setAttribute("aria-busy", String(visible));
10240
+ if (!visible) return;
10241
+ $("#chapter-line-numbers-inner").replaceChildren();
10242
+ $("#chapter-whitespace-inner").replaceChildren();
10243
+ }
10244
+
10245
+ function chapterEditingLocked() {
10246
+ return !state.chapter || String(chapterContentReadyId ?? "") !== String(state.chapter.id);
10247
+ }
10248
+
10249
+ function directoryChapterEntry(chapterId) {
10250
+ for (const volume of state.work?.volumes ?? []) {
10251
+ const chapter = volume.chapters?.find((item) => String(item.id) === String(chapterId));
10252
+ if (chapter) return chapter;
10228
10253
  }
10229
- resetChapterSearchPanel();
10230
- dismissDeleteToasts();
10231
- if (selectionGeneration !== chapterSelectionRequestGeneration || selectionRequestId !== chapterSelectionRequestId || state.work?.id !== workId) return false;
10232
- cancelChapterAutoSave();
10233
- let selectedChapter = state.chapter;
10234
- if (state.chapter?.id !== chapterId) {
10235
- selectedChapter = await api(`/api/chapters/${encodeURIComponent(chapterId)}`);
10236
- if (selectionGeneration !== chapterSelectionRequestGeneration || selectionRequestId !== chapterSelectionRequestId || state.work?.id !== workId) return false;
10237
- if (String(selectedChapter?.id ?? "") !== String(chapterId) || String(selectedChapter?.workId ?? "") !== String(workId)) return false;
10238
- }
10239
- if (!selectedChapter || selectionGeneration !== chapterSelectionRequestGeneration || selectionRequestId !== chapterSelectionRequestId || state.work?.id !== workId) return false;
10240
- state.chapter = selectedChapter;
10241
- mergeChapterDirectoryEntry(state.chapter);
10242
- state.collapsedVolumeIds.delete(state.chapter.volumeId);
10243
- lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
10244
- chapterEditorReadOnly = !canEditProse() || !editMode;
10254
+ return null;
10255
+ }
10256
+
10257
+ function chapterRequestStillCurrent(workId, selectionGeneration, selectionRequestId) {
10258
+ return selectionGeneration === chapterSelectionRequestGeneration
10259
+ && selectionRequestId === chapterSelectionRequestId
10260
+ && state.work?.id === workId;
10261
+ }
10262
+
10263
+ function presentChapter(chapter, { editMode = false, contentReady = false, syncRoute = true } = {}) {
10264
+ state.chapter = chapter;
10265
+ if (chapter?.volumeId && typeof chapter.content === "string" && (contentReady || chapter.content.length > 0)) {
10266
+ mergeChapterDirectoryEntry(state.chapter);
10267
+ }
10268
+ if (chapter?.volumeId) state.collapsedVolumeIds.delete(chapter.volumeId);
10269
+ lastSavedChapterSnapshot = {
10270
+ chapterId: state.chapter.id,
10271
+ title: state.chapter.title ?? "",
10272
+ content: state.chapter.content ?? ""
10273
+ };
10274
+ chapterContentReadyId = contentReady ? state.chapter.id : null;
10275
+ chapterEditorReadOnly = !contentReady || !editMode;
10245
10276
  state.module = "editor";
10246
10277
  applyWorkAccessMode();
10247
10278
  markActiveModule("editor");
@@ -10249,22 +10280,77 @@ async function selectChapter(chapterId, { editMode = false } = {}) {
10249
10280
  $("#module-view").classList.add("hidden");
10250
10281
  $("#editor-view").classList.remove("hidden");
10251
10282
  updateChapterPath();
10252
- $("#chapter-title").value = state.chapter.title;
10253
- $("#chapter-content").value = state.chapter.content;
10283
+ const titleInput = $("#chapter-title");
10284
+ const contentInput = $("#chapter-content");
10285
+ const nextTitle = state.chapter.title ?? "";
10286
+ const nextContent = state.chapter.content ?? "";
10287
+ if (titleInput.value !== nextTitle) titleInput.value = nextTitle;
10288
+ if (contentInput.value !== nextContent) contentInput.value = nextContent;
10254
10289
  resetChapterDraftLineIds(state.chapter);
10255
10290
  chapterAnnotationCounts = new Map();
10256
10291
  clearChapterLineSelection();
10257
10292
  scheduleChapterLineNumbers();
10258
- void loadChapterAnnotationCounts(state.chapter.id).catch((error) => {
10259
- if (String(state.chapter?.id ?? "") === String(chapterId)) toast("正文评论数量读取失败,请稍后重试", "error");
10260
- });
10261
10293
  dismissChapterInsightToast();
10262
10294
  updateChapterStats();
10295
+ if (!contentReady) $("#chapter-stats").textContent = "正在载入";
10263
10296
  if (!canEditProse()) setSaveState("正文只读");
10297
+ else if (!contentReady) setSaveState("正在同步正文");
10264
10298
  else if (chapterEditorReadOnly) setSaveState("阅读模式");
10265
10299
  else setSaveState("已保存");
10300
+ setChapterContentSkeleton(!contentReady);
10266
10301
  syncChapterTreeSelection();
10267
- replacePageRoute({ view: "editor", workId: state.work.id, chapterId: state.chapter.id });
10302
+ if (syncRoute) replacePageRoute({ view: "editor", workId: state.work.id, chapterId: state.chapter.id });
10303
+ }
10304
+
10305
+ async function selectChapter(chapterId, { editMode = false } = {}) {
10306
+ imWorkspace.close();
10307
+ const workId = state.work?.id;
10308
+ if (!workId) return false;
10309
+ const sameChapter = String(state.chapter?.id ?? "") === String(chapterId);
10310
+ if (sameChapter && chapterEditingLocked() && chapterBodyAbort && !chapterBodyAbort.signal.aborted) return true;
10311
+ const selectionGeneration = ++chapterSelectionRequestGeneration;
10312
+ const selectionRequestId = ++chapterSelectionRequestId;
10313
+ if (!sameChapter) clearChapterForeshadowReminders({ invalidateRequest: true });
10314
+ if (!sameChapter && !(await confirmDiscardChanges("当前章节有未保存修改,仍要切换吗?"))) {
10315
+ if (selectionRequestId === chapterSelectionRequestId) void loadChapterForeshadowReminders();
10316
+ return false;
10317
+ }
10318
+ resetChapterSearchPanel();
10319
+ dismissDeleteToasts();
10320
+ if (!chapterRequestStillCurrent(workId, selectionGeneration, selectionRequestId)) return false;
10321
+ cancelChapterAutoSave();
10322
+ if (sameChapter && String(chapterContentReadyId ?? "") === String(chapterId) && state.chapter) {
10323
+ presentChapter(state.chapter, { editMode, contentReady: true });
10324
+ syncChapterTreeSelection();
10325
+ return true;
10326
+ }
10327
+ const directoryEntry = directoryChapterEntry(chapterId);
10328
+ const previewChapter = directoryEntry
10329
+ ? { ...directoryEntry, content: "", lineIds: [] }
10330
+ : { id: chapterId, workId, volumeId: "", title: "", content: "", wordCount: 0, versionNo: 0, chapterType: "正文", lineIds: [] };
10331
+ presentChapter(previewChapter, { editMode: false, contentReady: false });
10332
+ syncChapterTreeSelection();
10333
+ chapterBodyAbort?.abort();
10334
+ const chapterBodyController = new AbortController();
10335
+ chapterBodyAbort = chapterBodyController;
10336
+ let selectedChapter = null;
10337
+ try {
10338
+ selectedChapter = await api(`/api/chapters/${encodeURIComponent(chapterId)}`, { signal: chapterBodyController.signal });
10339
+ } catch (error) {
10340
+ if (chapterBodyAbort === chapterBodyController) chapterBodyAbort = null;
10341
+ if (error?.name === "AbortError" || !chapterRequestStillCurrent(workId, selectionGeneration, selectionRequestId)) return false;
10342
+ if (String(state.chapter?.id ?? "") === String(chapterId)) setSaveState("正文载入失败", true);
10343
+ toast(error.message || "章节读取失败", "error");
10344
+ return false;
10345
+ }
10346
+ if (chapterBodyAbort === chapterBodyController) chapterBodyAbort = null;
10347
+ if (!chapterRequestStillCurrent(workId, selectionGeneration, selectionRequestId)) return false;
10348
+ if (String(selectedChapter?.id ?? "") !== String(chapterId) || String(selectedChapter?.workId ?? "") !== String(workId)) return false;
10349
+ presentChapter(selectedChapter, { editMode, contentReady: true, syncRoute: false });
10350
+ syncChapterTreeSelection();
10351
+ void loadChapterAnnotationCounts(state.chapter.id).catch(() => {
10352
+ if (String(state.chapter?.id ?? "") === String(chapterId)) toast("正文评论数量读取失败,请稍后重试", "error");
10353
+ });
10268
10354
  void loadChapterForeshadowReminders();
10269
10355
  return true;
10270
10356
  }
@@ -10836,6 +10922,8 @@ async function showModule(module) {
10836
10922
  state.module = module;
10837
10923
  if (module !== "editor") {
10838
10924
  chapterSelectionRequestId += 1;
10925
+ chapterBodyAbort?.abort();
10926
+ chapterBodyAbort = null;
10839
10927
  clearChapterForeshadowReminders({ invalidateRequest: true });
10840
10928
  }
10841
10929
  if (module !== "tasks") stopTaskProgressRefresh();
@@ -11897,12 +11985,13 @@ async function renderCharacters(page = characterListPage) {
11897
11985
  || characterFilters.genderValues.length > 0
11898
11986
  || characterFilters.deathState !== "all";
11899
11987
  const pageSize = pageSizeFor("characters");
11988
+ const needsCharacterFilterCatalog = hasCharacterFilters || characterFiltersPanelOpen;
11900
11989
  const [characterSource, races, organizations] = await Promise.all([
11901
11990
  hasCharacterFilters
11902
11991
  ? moduleApiAllPages("characters", `/api/works/${state.work.id}/characters`)
11903
11992
  : moduleApiPage("characters", `/api/works/${state.work.id}/characters`, page, pageSize),
11904
- canReadModule("races") ? moduleApi("characters", `/api/works/${state.work.id}/races`) : Promise.resolve([]),
11905
- canReadModule("organizations") ? moduleApiAllPages("characters", `/api/works/${state.work.id}/organizations`) : Promise.resolve([])
11993
+ needsCharacterFilterCatalog && canReadModule("races") ? moduleApi("characters", `/api/works/${state.work.id}/races`) : Promise.resolve([]),
11994
+ needsCharacterFilterCatalog && canReadModule("organizations") ? moduleApiAllPages("characters", `/api/works/${state.work.id}/organizations`) : Promise.resolve([])
11906
11995
  ]);
11907
11996
  const characterPage = hasCharacterFilters
11908
11997
  ? paginateCharacters(filterCharacters(characterSource, characterFilters), page, pageSize)
@@ -11910,7 +11999,7 @@ async function renderCharacters(page = characterListPage) {
11910
11999
  if (!characterPage.items.length && page > 1) return renderCharacters(page - 1);
11911
12000
  characterListPage = characterPage.page;
11912
12001
  const pageCharacters = characterPage.items;
11913
- [state.races, state.organizations] = [races, organizations];
12002
+ if (needsCharacterFilterCatalog) [state.races, state.organizations] = [races, organizations];
11914
12003
  mountModuleCount(characterPage.total);
11915
12004
  const layout = readModuleLayout();
11916
12005
  const characterActions = (item) => `${characterPinButton(item)}${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}`;
@@ -14164,10 +14253,7 @@ function updateBackgroundTaskCenterVisibility() {
14164
14253
  $("#background-task-count")?.classList.add("hidden");
14165
14254
  return;
14166
14255
  }
14167
- const activityCount = backgroundTaskActivityCount(
14168
- backgroundTaskCenterSnapshot.taskPage,
14169
- backgroundTaskCenterSnapshot.relationshipIndex
14170
- ) + (productUpdateChecking ? 1 : 0);
14256
+ const activityCount = backgroundTaskActivityCount(backgroundTaskCenterSnapshot.taskPage) + (productUpdateChecking ? 1 : 0);
14171
14257
  const badge = $("#background-task-count");
14172
14258
  badge.textContent = activityCount > 99 ? "99+" : String(activityCount);
14173
14259
  badge.classList.toggle("hidden", activityCount === 0);
@@ -14251,7 +14337,7 @@ function backgroundIndexMarkup(relationshipIndex, error) {
14251
14337
  const editable = canEditModule("ai-settings");
14252
14338
  return `<section class="background-task-section">
14253
14339
  <div class="background-task-section-heading">
14254
- <div><strong>人物关系拼音索引</strong><small>可在任何模块查看增量同步状态</small></div>
14340
+ <div><strong>人物关系拼音索引</strong><small>打开本窗口时显示当前索引状态</small></div>
14255
14341
  <div class="background-index-actions ${editable ? "" : "hidden"}">
14256
14342
  <button class="primary-button" type="button" data-background-index-action="sync">同步增量队列</button>
14257
14343
  <button class="ghost-button" type="button" data-background-index-action="rebuild">完整重建</button>
@@ -14322,10 +14408,7 @@ function scheduleBackgroundTaskCenterRefresh() {
14322
14408
  if (backgroundTaskCenterTimer !== null) window.clearTimeout(backgroundTaskCenterTimer);
14323
14409
  backgroundTaskCenterTimer = null;
14324
14410
  if (!backgroundTaskCenterWorkId) return;
14325
- const activityCount = backgroundTaskActivityCount(
14326
- backgroundTaskCenterSnapshot.taskPage,
14327
- backgroundTaskCenterSnapshot.relationshipIndex
14328
- );
14411
+ const activityCount = backgroundTaskActivityCount(backgroundTaskCenterSnapshot.taskPage);
14329
14412
  const delay = backgroundTaskPollDelay(activityCount, Boolean($("#background-task-dialog")?.open));
14330
14413
  backgroundTaskCenterTimer = window.setTimeout(() => {
14331
14414
  backgroundTaskCenterTimer = null;
@@ -14339,8 +14422,9 @@ async function refreshBackgroundTaskCenter({ announce = true } = {}) {
14339
14422
  if (backgroundTaskCenterTimer !== null) window.clearTimeout(backgroundTaskCenterTimer);
14340
14423
  backgroundTaskCenterTimer = null;
14341
14424
  const requestId = ++backgroundTaskCenterRequest;
14425
+ const dialogOpen = Boolean($("#background-task-dialog")?.open);
14342
14426
  const readTasks = canReadModule("tasks");
14343
- const readIndex = canReadModule("ai-settings");
14427
+ const readIndex = dialogOpen && canReadModule("ai-settings");
14344
14428
  const [taskResult, indexResult] = await Promise.all([
14345
14429
  readTasks
14346
14430
  ? api(`/api/works/${encodeURIComponent(workId)}/tasks?page=1&limit=30`)
@@ -20373,6 +20457,7 @@ async function showVersions() {
20373
20457
  }
20374
20458
  try {
20375
20459
  state.chapter = await api(`/api/chapters/${state.chapter.id}/restore`, { method: "POST", body: { versionNo: Number(button.dataset.restoreVersion) } });
20460
+ chapterContentReadyId = state.chapter.id;
20376
20461
  resetChapterDraftLineIds(state.chapter);
20377
20462
  lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
20378
20463
  $("#chapter-title").value = state.chapter.title;
@@ -21456,8 +21541,8 @@ $("#login-form").addEventListener("submit", async (event) => {
21456
21541
  window.location.reload();
21457
21542
  } catch (error) {
21458
21543
  $("#auth-error").textContent = error.message;
21459
- // 仅在验证码已显示时自动换一张,未加载过则保持默认隐藏状态
21460
- if (!$("#login-captcha-image").hidden) refreshAuthCaptcha("login").catch(() => {});
21544
+ // 答案错误时保留同一张图;过期、已核销或其他失败才换一张
21545
+ if (error?.code !== "CAPTCHA_INCORRECT" && !$("#login-captcha-image").hidden) refreshAuthCaptcha("login").catch(() => {});
21461
21546
  }
21462
21547
  });
21463
21548
  $("#register-form").addEventListener("submit", async (event) => {
@@ -21485,8 +21570,8 @@ $("#register-form").addEventListener("submit", async (event) => {
21485
21570
  window.location.reload();
21486
21571
  } catch (error) {
21487
21572
  $("#auth-error").textContent = error.message;
21488
- // 仅在验证码已显示时自动换一张,未加载过则保持默认隐藏状态
21489
- if (!$("#register-captcha-image").hidden) refreshAuthCaptcha("register").catch(() => {});
21573
+ // 答案错误时保留同一张图;过期、已核销或其他失败才换一张
21574
+ if (error?.code !== "CAPTCHA_INCORRECT" && !$("#register-captcha-image").hidden) refreshAuthCaptcha("register").catch(() => {});
21490
21575
  }
21491
21576
  });
21492
21577
  $("#settings-return").addEventListener("click", () => returnFromSettings().catch((error) => toast(error.message, "error")));
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
12
  <link rel="stylesheet" href="/mobile-app.css?v=20260922-mobile-offline-v2">
13
- <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&feature=ai-send-mode-v2&feature=character-attributes-prune-v1&feature=chapter-directory-click-priority-v1&feature=annotation-bubble-visibility-v1&feature=chapter-auto-title-menu-v1&feature=chapter-insert-menu-v1">
13
+ <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&feature=ai-send-mode-v2&feature=character-attributes-prune-v1&feature=chapter-directory-click-priority-v1&feature=annotation-bubble-visibility-v1&feature=chapter-auto-title-menu-v1&feature=chapter-insert-menu-v1&feature=chapter-switch-skeleton-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -330,6 +330,16 @@
330
330
  <div id="chapter-editor-scroll" class="chapter-editor-scroll">
331
331
  <aside id="chapter-line-numbers" class="chapter-line-numbers" aria-label="正文行号,可拖拽引用多行"><div id="chapter-line-numbers-inner"></div></aside>
332
332
  <textarea id="chapter-content" class="chapter-content" aria-label="章节正文" placeholder="在这里开始你的故事……"></textarea>
333
+ <div id="chapter-content-skeleton" class="chapter-content-skeleton hidden" role="status" aria-live="polite" aria-busy="false" aria-label="正在载入正文">
334
+ <span class="chapter-content-skeleton-line"></span>
335
+ <span class="chapter-content-skeleton-line"></span>
336
+ <span class="chapter-content-skeleton-line"></span>
337
+ <span class="chapter-content-skeleton-line"></span>
338
+ <span class="chapter-content-skeleton-line"></span>
339
+ <span class="chapter-content-skeleton-line"></span>
340
+ <span class="chapter-content-skeleton-line"></span>
341
+ <span class="chapter-content-skeleton-line"></span>
342
+ </div>
333
343
  <div id="chapter-whitespace-overlay" class="chapter-whitespace-overlay is-visible" aria-hidden="true"><div id="chapter-whitespace-inner"></div></div>
334
344
  </div>
335
345
  <div id="chapter-line-measure" class="chapter-line-measure" aria-hidden="true"></div>
@@ -1459,7 +1469,6 @@
1459
1469
  </dialog>
1460
1470
 
1461
1471
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1462
- <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&feature=ai-send-mode-v2&feature=character-attributes-prune-v1&feature=annotation-note-limit-v1&feature=chapter-directory-performance-v2&feature=chapter-directory-click-priority-v1&feature=mobile-offline-runtime-v6&feature=ai-title-first-turn-v1&feature=ai-optimistic-send-v1&feature=chapter-auto-title-menu-v1&feature=chapter-insert-menu-v1"></script>
1463
-
1472
+ <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&feature=ai-send-mode-v2&feature=character-attributes-prune-v1&feature=annotation-note-limit-v1&feature=chapter-directory-performance-v2&feature=chapter-directory-click-priority-v1&feature=chapter-switch-skeleton-v1&feature=mobile-offline-runtime-v6&feature=ai-title-first-turn-v1&feature=ai-optimistic-send-v1&feature=chapter-auto-title-menu-v1&feature=chapter-insert-menu-v1&feature=captcha-retry-same-image-v1&feature=relationship-index-dialog-poll-v1"></script>
1464
1473
  </body>
1465
1474
  </html>
@@ -1598,6 +1598,18 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1598
1598
  .chapter-content { position: relative; z-index: 1; grid-column: 2; grid-row: 1; resize: none; width: 100%; height: auto; min-height: 100%; margin: 0; padding: 32px 36px 72px; border: 0; background: transparent; border-radius: 0; font-size: var(--editor-font-size); line-height: var(--editor-line-height); letter-spacing: .01em; overflow-wrap: break-word; overflow: hidden; }
1599
1599
  .app-shell.editor-typewriter-mode .chapter-content { padding-bottom: max(72px, 45dvh); }
1600
1600
  .chapter-content:focus { box-shadow: none; }
1601
+ .chapter-content-skeleton { position: absolute; z-index: 3; grid-column: 2; grid-row: 1; display: grid; align-content: start; gap: 16px; width: 100%; min-height: 100%; margin: 0; padding: 36px 36px 72px; background: var(--paper); pointer-events: none; }
1602
+ .chapter-content-skeleton.hidden { display: none; }
1603
+ .chapter-content-skeleton-line { position: relative; display: block; height: 14px; overflow: hidden; border-radius: 4px; background: color-mix(in srgb, var(--line) 78%, var(--paper)); }
1604
+ .chapter-content-skeleton-line::after { position: absolute; inset: 0; background: linear-gradient(110deg, transparent 18%, color-mix(in srgb, var(--surface) 72%, transparent) 48%, transparent 78%); content: ""; transform: translateX(-110%); animation: image-upload-placeholder-shimmer 1.45s ease-in-out infinite; }
1605
+ .chapter-content-skeleton-line:nth-child(2) { width: 92%; }
1606
+ .chapter-content-skeleton-line:nth-child(3) { width: 84%; }
1607
+ .chapter-content-skeleton-line:nth-child(4) { width: 46%; }
1608
+ .chapter-content-skeleton-line:nth-child(5) { width: 96%; margin-top: 8px; }
1609
+ .chapter-content-skeleton-line:nth-child(6) { width: 88%; }
1610
+ .chapter-content-skeleton-line:nth-child(7) { width: 73%; }
1611
+ .chapter-content-skeleton-line:nth-child(8) { width: 38%; }
1612
+ @media (prefers-reduced-motion: reduce) { .chapter-content-skeleton-line::after { animation: none; } }
1601
1613
  .chapter-whitespace-overlay { position: relative; z-index: 2; grid-column: 2; grid-row: 1; width: 100%; height: 100%; overflow: hidden; opacity: 0; pointer-events: none; }
1602
1614
  .chapter-whitespace-overlay.is-visible { opacity: 1; }
1603
1615
  #chapter-whitespace-inner { box-sizing: border-box; min-height: 100%; color: transparent; white-space: pre-wrap; }
@@ -4575,6 +4587,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
4575
4587
  .editor-view .editor-actions > button { min-width: 0; min-height: 34px; padding-inline: 6px; font-size: 11px; }
4576
4588
  .chapter-title { font-size: 18px; }
4577
4589
  .chapter-content { padding: 22px 16px 64px 12px; font-size: max(16px, var(--editor-font-size)); }
4590
+ .chapter-content-skeleton { gap: 14px; padding: 26px 16px 64px 12px; }
4578
4591
  .app-shell.editor-typewriter-mode .chapter-content { padding-bottom: max(64px, 45dvh); }
4579
4592
  .chapter-line-numbers { width: 32px; font-size: 11px; }
4580
4593
  .chapter-editor-scroll { grid-template-columns: 32px minmax(0, 1fr); }
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- export const APP_VERSION = "1.1.4";
1
+ export const APP_VERSION = "1.1.5";
2
2
  export const SCRIVERSE_BETA_COMMIT_ENV = "SCRIVERSE_BETA_COMMIT";
3
3
  export function resolveBetaVersionLabel(environment) {
4
4
  const commit = environment[SCRIVERSE_BETA_COMMIT_ENV]?.trim().toLocaleLowerCase() ?? "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@musnows/scriverse",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "Command-line client and local server for the Scriverse long-form fiction workspace",
5
5
  "license": "AGPL-3.0-only",
6
6
  "author": "musnows",