@musnows/scriverse 1.1.3 → 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.
@@ -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">
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>
@@ -535,9 +545,19 @@
535
545
  <button type="button" role="menuitemradio" data-chapter-type="其他">其他</button>
536
546
  <div id="chapter-type-ai-reference-separator" class="line-citation-menu-separator hidden" role="separator" aria-orientation="horizontal"></div>
537
547
  <button type="button" role="menuitem" data-add-chapter-ai-reference>添加到助手引用</button>
548
+ <div id="chapter-insert-separator" class="line-citation-menu-separator hidden" role="separator" aria-orientation="horizontal"></div>
549
+ <button type="button" role="menuitem" data-insert-chapter-before>在章节上方插入新章节</button>
550
+ <button type="button" role="menuitem" data-insert-chapter-after>在章节下方插入新章节</button>
538
551
  <button class="danger-button" type="button" role="menuitem" data-delete-chapter>删除章节</button>
539
552
  </div>
540
553
  </div>
554
+ <div id="volume-context-menu" class="chapter-type-menu hidden" role="menu" aria-label="分卷操作">
555
+ <strong>分卷操作</strong>
556
+ <div>
557
+ <button type="button" role="menuitem" data-volume-menu-detail>分卷详情</button>
558
+ <button type="button" role="menuitem" data-volume-menu-numbering>标题编号</button>
559
+ </div>
560
+ </div>
541
561
  <div id="line-citation-menu" class="line-citation-menu hidden" role="menu" aria-label="正文行操作">
542
562
  <strong id="line-citation-label">已选择正文</strong>
543
563
  <button id="add-line-citation" type="button" role="menuitem">添加到创作助手引用</button>
@@ -1449,7 +1469,6 @@
1449
1469
  </dialog>
1450
1470
 
1451
1471
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1452
- <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"></script>
1453
-
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>
1454
1473
  </body>
1455
1474
  </html>
@@ -1434,6 +1434,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1434
1434
  .module-nav .ai-assistant-entry.active { background: color-mix(in srgb, var(--accent) 10%, var(--surface)); color: var(--accent-dark); font-weight: 650; }
1435
1435
  #module-more-button { grid-column: 2; color: var(--accent-dark); text-align: center; }
1436
1436
  .panel-heading { display: flex; align-items: center; gap: 8px; padding: 15px 0 9px 7px; color: var(--muted); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
1437
+ .dialog-fields select[name="chapterTitleFormat"] { text-transform: none; letter-spacing: 0; }
1437
1438
  .panel-heading-label { min-width: 0; white-space: nowrap; }
1438
1439
  .panel-heading-label-compact { display: none; }
1439
1440
  .panel-heading-actions { display: flex; align-items: center; gap: 7px; margin-left: auto; white-space: nowrap; }
@@ -1597,6 +1598,18 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1597
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; }
1598
1599
  .app-shell.editor-typewriter-mode .chapter-content { padding-bottom: max(72px, 45dvh); }
1599
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; } }
1600
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; }
1601
1614
  .chapter-whitespace-overlay.is-visible { opacity: 1; }
1602
1615
  #chapter-whitespace-inner { box-sizing: border-box; min-height: 100%; color: transparent; white-space: pre-wrap; }
@@ -3538,10 +3551,10 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3538
3551
  .chapter-insight-toast-detail { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 8px; padding-top: 9px; border-top: 1px solid var(--line); }
3539
3552
  .chapter-insight-toast-detail strong { color: var(--ink); font-size: 10px; }
3540
3553
  .chapter-insight-toast-empty { color: var(--muted); }
3541
- .chapter-type-menu { position: fixed; z-index: 80; width: 176px; padding: 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); box-shadow: 0 14px 38px rgba(49,42,32,.22); }
3554
+ .chapter-type-menu { position: fixed; z-index: 80; width: 220px; padding: 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); box-shadow: 0 14px 38px rgba(49,42,32,.22); }
3542
3555
  .chapter-type-menu strong { display: block; overflow: hidden; margin: 0 2px 8px; color: var(--muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
3543
3556
  .chapter-type-menu > div { display: grid; gap: 3px; }
3544
- .chapter-type-menu button { border: 0; border-radius: 3px; padding: 7px 8px; background: transparent; text-align: left; font-size: 11px; }
3557
+ .chapter-type-menu button { border: 0; border-radius: 3px; padding: 7px 8px; background: transparent; text-align: left; font-size: 11px; white-space: nowrap; }
3545
3558
  .chapter-type-menu button:hover, .chapter-type-menu button.active { background: var(--paper-deep); color: var(--accent-dark); }
3546
3559
  .chapter-type-menu .danger-button { margin-top: 5px; border-top: 1px solid var(--line); border-radius: 0; color: var(--accent-dark); }
3547
3560
  .line-citation-menu { position: fixed; z-index: 82; display: grid; min-width: 230px; padding: 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); box-shadow: 0 14px 38px rgba(49,42,32,.22); }
@@ -4574,6 +4587,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
4574
4587
  .editor-view .editor-actions > button { min-width: 0; min-height: 34px; padding-inline: 6px; font-size: 11px; }
4575
4588
  .chapter-title { font-size: 18px; }
4576
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; }
4577
4591
  .app-shell.editor-typewriter-mode .chapter-content { padding-bottom: max(64px, 45dvh); }
4578
4592
  .chapter-line-numbers { width: 32px; font-size: 11px; }
4579
4593
  .chapter-editor-scroll { grid-template-columns: 32px minmax(0, 1fr); }
package/dist/store.js CHANGED
@@ -16,7 +16,7 @@ import { DEFAULT_AI_STREAM_IDLE_TIMEOUT_SECONDS, normalizeAiStreamIdleTimeoutSec
16
16
  import { normalizeCharacterAttributes, normalizeCharacterState } from "./public/character-profile.js";
17
17
  import { normalizeRoleplayScenePin, roleplayUserTurnDisplayText, roleplayUserTurnTitleSource } from "./roleplay-turn.js";
18
18
  import { chapterAnnotationLineHashes, createChapterLineIds, MAX_CHAPTER_LINE_IDS, parseChapterAnnotationLineIds, parseChapterAnnotationLineHashes, parseChapterLineIds, reconcileChapterLineIds, reanchorChapterAnnotations } from "./chapter-annotation-anchor.js";
19
- import { renumberChapterTitle } from "./chapter-title-numbering.js";
19
+ import { chapterTitleFormats, isChapterTitlePreference, parseChapterTitleNumber, planAutoNumberedTitles, planVolumeTitleRenumber, renumberChapterTitle, resolveChapterTitleFormat } from "./chapter-title-numbering.js";
20
20
  import { normalizeRoleplayMemoryContent, roleplayMemoryCandidateIsSafe } from "./roleplay-memory.js";
21
21
  const WORK_LIST_BATCH_SIZE = 500;
22
22
  const ENTITY_LIST_BATCH_SIZE = 400;
@@ -578,6 +578,7 @@ export class Store {
578
578
  tags: entity.tags,
579
579
  editorAutoIndentEnabled: entity.editorAutoIndentEnabled,
580
580
  editorTypewriterModeEnabled: entity.editorTypewriterModeEnabled,
581
+ chapterTitleFormat: entity.chapterTitleFormat,
581
582
  ownerUserId: entity.ownerUserId
582
583
  };
583
584
  if (type === "volume")
@@ -847,8 +848,8 @@ export class Store {
847
848
  const ownerUserId = this.resolveWorkOwnerUserId(typeof snapshot.ownerUserId === "string" ? snapshot.ownerUserId : null, true);
848
849
  const timestamp = now();
849
850
  this.db.run(`INSERT INTO works (id, title, author, description, language, cover_url, tags_json,
850
- editor_auto_indent_enabled, editor_typewriter_mode_enabled, version_no, created_at, updated_at, owner_user_id)
851
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)`, entityId, String(snapshot.title ?? "未命名作品"), String(snapshot.author ?? ""), String(snapshot.description ?? ""), String(snapshot.language ?? "zh-CN"), snapshot.coverUrl ?? null, JSON.stringify(Array.isArray(snapshot.tags) ? snapshot.tags : []), snapshot.editorAutoIndentEnabled === true ? 1 : 0, snapshot.editorTypewriterModeEnabled === true ? 1 : 0, timestamp, timestamp, ownerUserId);
851
+ editor_auto_indent_enabled, editor_typewriter_mode_enabled, chapter_title_format, version_no, created_at, updated_at, owner_user_id)
852
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?)`, entityId, String(snapshot.title ?? "未命名作品"), String(snapshot.author ?? ""), String(snapshot.description ?? ""), String(snapshot.language ?? "zh-CN"), snapshot.coverUrl ?? null, JSON.stringify(Array.isArray(snapshot.tags) ? snapshot.tags : []), snapshot.editorAutoIndentEnabled === true ? 1 : 0, snapshot.editorTypewriterModeEnabled === true ? 1 : 0, isChapterTitlePreference(String(snapshot.chapterTitleFormat ?? "")) ? String(snapshot.chapterTitleFormat) : "off", timestamp, timestamp, ownerUserId);
852
853
  if (ownerUserId !== SYSTEM_USER_ID) {
853
854
  this.db.run("INSERT INTO work_memberships (work_id, user_id, role, invited_by_user_id, created_at) VALUES (?, ?, 'owner', ?, ?)", entityId, ownerUserId, ownerUserId, timestamp);
854
855
  }
@@ -912,10 +913,12 @@ export class Store {
912
913
  const workId = id("work");
913
914
  const timestamp = now();
914
915
  const resolvedOwnerUserId = this.resolveWorkOwnerUserId(ownerUserId);
916
+ const requestedTitleFormat = input.chapterTitleFormat ?? "";
917
+ const chapterTitleFormat = isChapterTitlePreference(requestedTitleFormat) ? requestedTitleFormat : "off";
915
918
  this.db.transaction(() => {
916
919
  this.db.run(`INSERT INTO works (id, title, author, description, language, cover_url, tags_json,
917
- editor_auto_indent_enabled, editor_typewriter_mode_enabled, created_at, updated_at, owner_user_id)
918
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, workId, input.title, input.author ?? "", input.description ?? "", input.language ?? "zh-CN", input.coverUrl ?? null, JSON.stringify(input.tags ?? []), input.editorAutoIndentEnabled === true ? 1 : 0, input.editorTypewriterModeEnabled === true ? 1 : 0, timestamp, timestamp, resolvedOwnerUserId);
920
+ editor_auto_indent_enabled, editor_typewriter_mode_enabled, chapter_title_format, created_at, updated_at, owner_user_id)
921
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, workId, input.title, input.author ?? "", input.description ?? "", input.language ?? "zh-CN", input.coverUrl ?? null, JSON.stringify(input.tags ?? []), input.editorAutoIndentEnabled === true ? 1 : 0, input.editorTypewriterModeEnabled === true ? 1 : 0, chapterTitleFormat, timestamp, timestamp, resolvedOwnerUserId);
919
922
  if (resolvedOwnerUserId !== SYSTEM_USER_ID) {
920
923
  this.db.run("INSERT INTO work_memberships (work_id, user_id, role, invited_by_user_id, created_at) VALUES (?, ?, 'owner', ?, ?)", workId, resolvedOwnerUserId, resolvedOwnerUserId, timestamp);
921
924
  }
@@ -1356,8 +1359,8 @@ export class Store {
1356
1359
  this.assertExpectedVersion("work", workId, expectedVersionNo, "作品", Number(current.versionNo));
1357
1360
  const timestamp = now();
1358
1361
  this.db.run(`UPDATE works SET title = ?, author = ?, description = ?, language = ?, cover_url = ?, tags_json = ?,
1359
- editor_auto_indent_enabled = ?, editor_typewriter_mode_enabled = ?, version_no = version_no + 1, updated_at = ?
1360
- WHERE id = ?`, input.title ?? String(current.title), input.author ?? String(current.author), input.description ?? String(current.description), input.language ?? String(current.language), input.coverUrl === undefined ? current.coverUrl : input.coverUrl, JSON.stringify(input.tags ?? current.tags), input.editorAutoIndentEnabled === undefined ? (current.editorAutoIndentEnabled ? 1 : 0) : input.editorAutoIndentEnabled ? 1 : 0, input.editorTypewriterModeEnabled === undefined ? (current.editorTypewriterModeEnabled ? 1 : 0) : input.editorTypewriterModeEnabled ? 1 : 0, timestamp, workId);
1362
+ editor_auto_indent_enabled = ?, editor_typewriter_mode_enabled = ?, chapter_title_format = ?, version_no = version_no + 1, updated_at = ?
1363
+ WHERE id = ?`, input.title ?? String(current.title), input.author ?? String(current.author), input.description ?? String(current.description), input.language ?? String(current.language), input.coverUrl === undefined ? current.coverUrl : input.coverUrl, JSON.stringify(input.tags ?? current.tags), input.editorAutoIndentEnabled === undefined ? (current.editorAutoIndentEnabled ? 1 : 0) : input.editorAutoIndentEnabled ? 1 : 0, input.editorTypewriterModeEnabled === undefined ? (current.editorTypewriterModeEnabled ? 1 : 0) : input.editorTypewriterModeEnabled ? 1 : 0, input.chapterTitleFormat === undefined ? String(current.chapterTitleFormat ?? "off") : input.chapterTitleFormat, timestamp, workId);
1361
1364
  this.recordEntityVersion("work", workId, source, sourceRef, changeNote || "更新作品信息", timestamp);
1362
1365
  this.audit(workId, "work.updated", "work", workId, { fields: Object.keys(input), versionNo: Number(current.versionNo) + 1, source, sourceRef, changeNote });
1363
1366
  });
@@ -2081,16 +2084,190 @@ export class Store {
2081
2084
  }
2082
2085
  createChapter(workId, input) {
2083
2086
  return this.db.transaction(() => {
2087
+ if (input.insertAfterChapterId && input.insertBeforeChapterId) {
2088
+ throw new AppError(400, "CHAPTER_INSERT_ANCHOR_CONFLICT", "不能同时指定章节前后插入位置");
2089
+ }
2084
2090
  this.getWork(workId);
2085
2091
  const volume = this.getVolume(input.volumeId);
2086
2092
  if (volume.workId !== workId)
2087
2093
  throw new AppError(400, "VOLUME_WORK_MISMATCH", "卷不属于当前作品");
2088
- const last = this.db.get("SELECT COALESCE(MAX(sort_order), -1) AS value FROM chapters WHERE volume_id = ? AND deleted_at IS NULL", input.volumeId);
2089
- const chapterId = this.insertChapter(workId, input.volumeId, input.title, input.content ?? "", numberValue(last ?? {}, "value") + 1, "manual", null, input.chapterType ?? "正文");
2090
- this.audit(workId, "chapter.created", "chapter", chapterId);
2094
+ const timestamp = now();
2095
+ const rows = this.listActiveChapterRows(workId, input.volumeId);
2096
+ let sortOrder = numberValue(this.db.get("SELECT COALESCE(MAX(sort_order), -1) AS value FROM chapters WHERE volume_id = ? AND deleted_at IS NULL", input.volumeId) ?? {}, "value") + 1;
2097
+ let insertIndex = rows.length;
2098
+ const anchorId = input.insertBeforeChapterId ?? input.insertAfterChapterId;
2099
+ if (anchorId) {
2100
+ const anchor = this.getChapter(anchorId);
2101
+ if (anchor.workId !== workId)
2102
+ throw new AppError(400, "CHAPTER_WORK_MISMATCH", "章节不属于当前作品");
2103
+ const anchorIndex = String(anchor.volumeId) === input.volumeId
2104
+ ? rows.findIndex((row) => requiredString(row, "id") === anchorId)
2105
+ : -1;
2106
+ if (anchorIndex >= 0) {
2107
+ const before = Boolean(input.insertBeforeChapterId);
2108
+ sortOrder = Number(anchor.sortOrder) + (before ? 0 : 1);
2109
+ insertIndex = anchorIndex + (before ? 0 : 1);
2110
+ this.db.run("UPDATE chapters SET sort_order = sort_order + 1, updated_at = ? WHERE volume_id = ? AND deleted_at IS NULL AND sort_order >= ?", timestamp, input.volumeId, sortOrder);
2111
+ }
2112
+ }
2113
+ const pendingId = "__pending_chapter__";
2114
+ const ordered = [
2115
+ ...rows.slice(0, insertIndex).map((row) => ({ id: requiredString(row, "id"), title: requiredString(row, "title") })),
2116
+ { id: pendingId, title: input.title },
2117
+ ...rows.slice(insertIndex).map((row) => ({ id: requiredString(row, "id"), title: requiredString(row, "title") }))
2118
+ ];
2119
+ const formatId = input.numberTitle ? this.resolveWorkChapterTitleFormat(workId, ordered.map((chapter) => chapter.title)) : null;
2120
+ let title = input.title;
2121
+ let numberingUpdates = [];
2122
+ if (formatId) {
2123
+ try {
2124
+ numberingUpdates = planAutoNumberedTitles(ordered, pendingId, input.title, formatId);
2125
+ }
2126
+ catch (error) {
2127
+ this.rethrowChapterNumberError(error);
2128
+ }
2129
+ title = numberingUpdates.find((item) => item.id === pendingId)?.title ?? input.title;
2130
+ numberingUpdates = numberingUpdates.filter((item) => item.id !== pendingId);
2131
+ if (title.length > 300)
2132
+ throw new AppError(400, "CHAPTER_TITLE_TOO_LONG", "编号后的章节标题超过 300 个字符");
2133
+ }
2134
+ const chapterId = this.insertChapter(workId, input.volumeId, title, input.content ?? "", sortOrder, "manual", null, input.chapterType ?? "正文");
2135
+ if (numberingUpdates.length) {
2136
+ const shiftedRows = this.listActiveChapterRows(workId, input.volumeId);
2137
+ this.writeChapterTitleUpdates(workId, numberingUpdates, shiftedRows, "自动顺延章节序号", timestamp);
2138
+ }
2139
+ this.audit(workId, "chapter.created", "chapter", chapterId, formatId ? { chapterTitleFormat: formatId, numbered: numberingUpdates.length > 0 || title !== input.title } : {});
2140
+ return this.getChapter(chapterId);
2141
+ });
2142
+ }
2143
+ describeChapterTitleFormat(workId) {
2144
+ const work = this.getWork(workId);
2145
+ const preference = isChapterTitlePreference(String(work.chapterTitleFormat ?? ""))
2146
+ ? String(work.chapterTitleFormat)
2147
+ : "off";
2148
+ const detected = resolveChapterTitleFormat("auto", this.listActiveChapterRows(workId).map((row) => requiredString(row, "title")));
2149
+ return {
2150
+ preference,
2151
+ detected,
2152
+ resolved: resolveChapterTitleFormat(preference, this.listActiveChapterRows(workId).map((row) => requiredString(row, "title")))
2153
+ };
2154
+ }
2155
+ applyChapterTitleNumber(chapterId, title, expectedVersionNo) {
2156
+ return this.db.transaction(() => {
2157
+ const current = this.getChapter(chapterId);
2158
+ this.assertExpectedRevision("chapter", chapterId, expectedVersionNo, "章节", Number(current.versionNo));
2159
+ const workId = String(current.workId);
2160
+ const volumeId = String(current.volumeId);
2161
+ const rows = this.listActiveChapterRows(workId, volumeId);
2162
+ const formatId = this.resolveWorkChapterTitleFormat(workId, rows.map((row) => requiredString(row, "title")));
2163
+ const savedNumber = parseChapterTitleNumber(String(current.title));
2164
+ if (!formatId || parseChapterTitleNumber(title) || savedNumber) {
2165
+ const nextTitle = formatId && savedNumber && !parseChapterTitleNumber(title)
2166
+ ? renumberChapterTitle(title, savedNumber.number, chapterTitleFormats[savedNumber.formatId].template, chapterTitleFormats[savedNumber.formatId].style)
2167
+ : title;
2168
+ return this.saveChapter(chapterId, { title: nextTitle }, "manual", null, nextTitle === title ? "" : "补回章节序号", expectedVersionNo);
2169
+ }
2170
+ const ordered = rows.map((row) => ({
2171
+ id: requiredString(row, "id"),
2172
+ title: requiredString(row, "id") === chapterId ? title : requiredString(row, "title")
2173
+ }));
2174
+ let updates;
2175
+ try {
2176
+ updates = planAutoNumberedTitles(ordered, chapterId, title, formatId);
2177
+ }
2178
+ catch (error) {
2179
+ this.rethrowChapterNumberError(error);
2180
+ }
2181
+ const timestamp = now();
2182
+ this.writeChapterTitleUpdates(workId, updates, rows, "补写章节序号", timestamp);
2183
+ this.db.run("UPDATE works SET updated_at = ? WHERE id = ?", timestamp, workId);
2091
2184
  return this.getChapter(chapterId);
2092
2185
  });
2093
2186
  }
2187
+ renumberVolumeChapterTitles(volumeId) {
2188
+ return this.db.transaction(() => {
2189
+ const volume = this.getVolume(volumeId);
2190
+ const workId = String(volume.workId);
2191
+ const rows = this.listActiveChapterRows(workId, volumeId);
2192
+ const formatId = this.resolveWorkChapterTitleFormat(workId, rows.map((row) => requiredString(row, "title")));
2193
+ if (!formatId)
2194
+ throw new AppError(400, "CHAPTER_TITLE_FORMAT_REQUIRED", "请先在目录的标题编号中选择格式,或使用自动识别");
2195
+ let plan;
2196
+ try {
2197
+ plan = planVolumeTitleRenumber(rows.map((row) => ({ id: requiredString(row, "id"), title: requiredString(row, "title") })), formatId);
2198
+ }
2199
+ catch (error) {
2200
+ this.rethrowChapterNumberError(error);
2201
+ }
2202
+ const timestamp = now();
2203
+ const updated = this.writeChapterTitleUpdates(workId, plan.updates, rows, "按分卷重排章节标题序号", timestamp);
2204
+ this.db.run("UPDATE works SET updated_at = ? WHERE id = ?", timestamp, workId);
2205
+ this.audit(workId, "chapter.volume-renumbered", "volume", volumeId, {
2206
+ format: formatId,
2207
+ startAt: plan.startAt,
2208
+ updated,
2209
+ chapterCount: rows.length
2210
+ });
2211
+ return { volumeId, updated, format: formatId, startAt: plan.startAt };
2212
+ });
2213
+ }
2214
+ resolveWorkChapterTitleFormat(workId, titles) {
2215
+ const preference = String(this.getWork(workId).chapterTitleFormat ?? "off");
2216
+ return resolveChapterTitleFormat(preference, titles);
2217
+ }
2218
+ listActiveChapterRows(workId, volumeId) {
2219
+ return this.db.all(`SELECT chapter.* FROM chapters chapter
2220
+ JOIN volumes volume ON volume.id = chapter.volume_id
2221
+ WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL AND volume.deleted_at IS NULL
2222
+ ${volumeId ? "AND chapter.volume_id = ?" : ""}
2223
+ ORDER BY volume.sort_order, volume.created_at, volume.id,
2224
+ chapter.sort_order, chapter.created_at, chapter.id`, ...(volumeId ? [workId, volumeId] : [workId]));
2225
+ }
2226
+ rethrowChapterNumberError(error) {
2227
+ if (error instanceof RangeError)
2228
+ throw new AppError(400, "CHAPTER_NUMBER_RANGE", "章节序号超出 1 到 999999");
2229
+ throw error;
2230
+ }
2231
+ writeChapterTitleUpdates(workId, updates, rows, changeNote, timestamp) {
2232
+ const rowsById = new Map(rows.map((row) => [requiredString(row, "id"), row]));
2233
+ let updated = 0;
2234
+ for (const item of updates) {
2235
+ const row = rowsById.get(item.id);
2236
+ if (!row || requiredString(row, "title") === item.title)
2237
+ continue;
2238
+ if (item.title.length > 300) {
2239
+ throw new AppError(400, "CHAPTER_TITLE_TOO_LONG", `章节“${requiredString(row, "title").slice(0, 40)}”编号后的标题超过 300 个字符`);
2240
+ }
2241
+ const chapterId = item.id;
2242
+ const versionNo = numberValue(row, "version_no") + 1;
2243
+ this.db.run("UPDATE chapters SET title = ?, version_no = ?, analysis_status = 'expired', updated_at = ? WHERE id = ?", item.title, versionNo, timestamp, chapterId);
2244
+ this.syncChapterParagraphSearchVersion(chapterId, versionNo);
2245
+ this.insertChapterVersionRow({
2246
+ workId,
2247
+ chapterId,
2248
+ versionNo,
2249
+ title: item.title,
2250
+ content: requiredString(row, "content"),
2251
+ volumeId: requiredString(row, "volume_id"),
2252
+ sortOrder: numberValue(row, "sort_order"),
2253
+ chapterType: requiredString(row, "chapter_type"),
2254
+ source: "manual",
2255
+ sourceRef: null,
2256
+ changeNote,
2257
+ timestamp
2258
+ });
2259
+ this.invalidateChapter(workId, chapterId, versionNo);
2260
+ this.audit(workId, "chapter.saved", "chapter", chapterId, {
2261
+ previousTitle: requiredString(row, "title"),
2262
+ title: item.title,
2263
+ sequence: item.sequence,
2264
+ versionNo,
2265
+ renumbered: true
2266
+ });
2267
+ updated += 1;
2268
+ }
2269
+ return updated;
2270
+ }
2094
2271
  getChapter(chapterId) {
2095
2272
  const row = this.db.get(`SELECT chapter.* FROM chapters chapter
2096
2273
  JOIN volumes volume ON volume.id = chapter.volume_id
@@ -3373,6 +3550,9 @@ export class Store {
3373
3550
  offlineAccessEnabled: numberValue(row, "offline_access_enabled") === 1,
3374
3551
  editorAutoIndentEnabled: numberValue(row, "editor_auto_indent_enabled") === 1,
3375
3552
  editorTypewriterModeEnabled: numberValue(row, "editor_typewriter_mode_enabled") === 1,
3553
+ chapterTitleFormat: isChapterTitlePreference(optionalString(row, "chapter_title_format") ?? "")
3554
+ ? optionalString(row, "chapter_title_format")
3555
+ : "off",
3376
3556
  versionNo: numberValue(row, "version_no") || this.currentEntityVersionNo("work", workId),
3377
3557
  ownerUserId,
3378
3558
  accessRole,