@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.
- package/dist/app.js +37 -3
- package/dist/app.js.map +1 -1
- package/dist/chapter-title-numbering.js +210 -0
- package/dist/chapter-title-numbering.js.map +1 -1
- package/dist/cli-contract.js +15 -6
- package/dist/cli-contract.js.map +1 -1
- package/dist/cli-core.js +37 -1
- package/dist/cli-core.js.map +1 -1
- package/dist/database.js +21 -2
- package/dist/database.js.map +1 -1
- package/dist/image-captcha.js +35 -9
- package/dist/image-captcha.js.map +1 -1
- package/dist/public/app.js +509 -65
- package/dist/public/index.html +22 -3
- package/dist/public/styles.css +16 -2
- package/dist/store.js +190 -10
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +5 -1
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/public/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
12
|
<link rel="stylesheet" href="/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>
|
package/dist/public/styles.css
CHANGED
|
@@ -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:
|
|
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
|
|
2089
|
-
const
|
|
2090
|
-
this.
|
|
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,
|