@musnows/scriverse 0.9.1 → 0.9.2

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.
@@ -1052,9 +1052,14 @@ function renderPresence() {
1052
1052
  return;
1053
1053
  }
1054
1054
  const groups = groupedPresenceParticipants();
1055
+ const showControl = groups.length > 1;
1055
1056
  const localKey = presencePageKey(presencePageForRoute());
1056
1057
  syncChapterAutoSaveWithPresence();
1057
- control.classList.remove("hidden");
1058
+ control.classList.toggle("hidden", !showControl);
1059
+ if (!showControl) {
1060
+ $("#presence-panel").classList.add("hidden");
1061
+ $("#presence-button").setAttribute("aria-expanded", "false");
1062
+ }
1058
1063
  $("#presence-count").textContent = `${groups.length} 人`;
1059
1064
  $("#presence-list").innerHTML = groups.map((participant) => {
1060
1065
  const isCurrent = participant.userId === state.user?.userId;
@@ -4735,6 +4740,7 @@ function uploadWithProgress(path, options = {}, onProgress = () => {}) {
4735
4740
  request.upload.addEventListener("progress", (event) => {
4736
4741
  onProgress(event.lengthComputable && event.total > 0 ? (event.loaded / event.total) * 100 : null);
4737
4742
  });
4743
+ request.upload.addEventListener("load", () => onProgress(100));
4738
4744
  request.addEventListener("error", () => {
4739
4745
  updateSystemHealth({ status: "offline" });
4740
4746
  reject(new Error("网络连接失败"));
@@ -5395,6 +5401,56 @@ function persistentToast(message, type = "info") {
5395
5401
  return () => dismissToastElement(element);
5396
5402
  }
5397
5403
 
5404
+ function createImportProgressToast(fileName) {
5405
+ const region = $("#import-progress-region");
5406
+ const element = document.createElement("div");
5407
+ element.className = "toast import-progress-toast";
5408
+ element.setAttribute("role", "status");
5409
+ element.setAttribute("aria-atomic", "true");
5410
+ const copy = document.createElement("div");
5411
+ copy.className = "import-progress-copy";
5412
+ const title = document.createElement("strong");
5413
+ title.textContent = `正在导入“${fileName}”`;
5414
+ const status = document.createElement("span");
5415
+ status.className = "import-progress-status";
5416
+ status.textContent = "正在上传 · 0%";
5417
+ const progress = document.createElement("progress");
5418
+ progress.max = 100;
5419
+ progress.value = 0;
5420
+ progress.setAttribute("aria-label", "书籍导入上传进度");
5421
+ copy.append(title, status);
5422
+ element.append(copy, progress);
5423
+ region.append(element);
5424
+ if (typeof region.showPopover === "function" && !region.matches(":popover-open")) region.showPopover();
5425
+ let closed = false;
5426
+ return {
5427
+ update(uploadProgress) {
5428
+ if (closed) return;
5429
+ if (Number.isFinite(uploadProgress) && uploadProgress >= 100) {
5430
+ progress.removeAttribute("value");
5431
+ status.textContent = "上传完成,正在解析并写入作品…";
5432
+ return;
5433
+ }
5434
+ if (Number.isFinite(uploadProgress)) {
5435
+ const percentage = Math.max(0, Math.min(99, Math.round(uploadProgress)));
5436
+ progress.value = percentage;
5437
+ status.textContent = `正在上传 · ${percentage}%`;
5438
+ } else {
5439
+ progress.removeAttribute("value");
5440
+ status.textContent = "正在上传…";
5441
+ }
5442
+ },
5443
+ close() {
5444
+ if (closed) return;
5445
+ closed = true;
5446
+ element.remove();
5447
+ if (!region.childElementCount && typeof region.hidePopover === "function" && region.matches(":popover-open")) {
5448
+ region.hidePopover();
5449
+ }
5450
+ }
5451
+ };
5452
+ }
5453
+
5398
5454
  function restoreToastFocus(previousFocus) {
5399
5455
  if (
5400
5456
  previousFocus instanceof HTMLElement
@@ -18853,15 +18909,22 @@ $("#import-file").addEventListener("change", async (event) => {
18853
18909
  body.append("file", file);
18854
18910
  body.append("mode", mode);
18855
18911
  body.append("expectedVersionNo", String(state.work.versionNo));
18912
+ const importProgress = createImportProgressToast(file.name);
18856
18913
  try {
18857
- const result = await api(`/api/works/${state.work.id}/import`, { method: "POST", body });
18914
+ const result = await uploadWithProgress(
18915
+ `/api/works/${state.work.id}/import`,
18916
+ { method: "POST", body },
18917
+ (progress) => importProgress.update(progress)
18918
+ );
18858
18919
  setSaveState(mode === "append" ? "已追加" : "已覆盖");
18859
18920
  state.work = result.tree;
18860
18921
  renderTree();
18861
18922
  const completion = mode === "append" ? "正文追加完成" : "正文覆盖完成";
18862
- toast(result.warnings.length ? `${completion}:${result.warnings.join(";")}` : completion);
18863
18923
  if (result.firstImportedChapterId) await selectChapter(result.firstImportedChapterId);
18924
+ importProgress.close();
18925
+ toast(result.warnings.length ? `${completion}:${result.warnings.join(";")}` : completion);
18864
18926
  } catch (error) {
18927
+ importProgress.close();
18865
18928
  toast(error.message, "error");
18866
18929
  if (state.dirty) scheduleChapterAutoSave();
18867
18930
  }
@@ -18876,11 +18939,18 @@ $("#new-import-file").addEventListener("change", async (event) => {
18876
18939
  body.append("title", metadata.title ?? "");
18877
18940
  body.append("author", metadata.author ?? "");
18878
18941
  body.append("description", metadata.description ?? "");
18942
+ const importProgress = createImportProgressToast(file.name);
18879
18943
  try {
18880
- const result = await api("/api/works/import", { method: "POST", body });
18881
- toast(result.warnings.length ? `作品已导入:${result.warnings.join("")}` : "作品已导入");
18944
+ const result = await uploadWithProgress(
18945
+ "/api/works/import",
18946
+ { method: "POST", body },
18947
+ (progress) => importProgress.update(progress)
18948
+ );
18882
18949
  await loadWorks(result.work.id);
18950
+ importProgress.close();
18951
+ toast(result.warnings.length ? `作品已导入:${result.warnings.join(";")}` : "作品已导入");
18883
18952
  } catch (error) {
18953
+ importProgress.close();
18884
18954
  toast(error.message, "error");
18885
18955
  } finally {
18886
18956
  state.pendingImportMeta = null;
@@ -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="/vendor/vditor/dist/index.css?v=3.11.2">
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=annotation-line-counts-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=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-v3&feature=character-card-title-fit-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=annotation-line-counts-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=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">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -476,6 +476,7 @@
476
476
  </section>
477
477
 
478
478
  <div id="toast-region" class="toast-region" data-position="bottom-right" aria-live="polite" popover="manual"></div>
479
+ <div id="import-progress-region" class="toast-region import-progress-region" data-position="bottom-right" aria-live="polite" popover="manual"></div>
479
480
  <div id="chapter-type-menu" class="chapter-type-menu hidden" role="menu" aria-label="章节操作" data-testid="chapter-type-menu">
480
481
  <strong>标记章节</strong>
481
482
  <div>
@@ -1261,6 +1262,6 @@
1261
1262
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1262
1263
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
1263
1264
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
1264
- <script type="module" src="/app.js?v=20260816-extended-thinking-effort-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-v1&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"></script>
1265
+ <script type="module" src="/app.js?v=20260816-extended-thinking-effort-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-v1&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"></script>
1265
1266
  </body>
1266
1267
  </html>
@@ -1589,22 +1589,19 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1589
1589
  .module-row-span { grid-column: 1 / -1; }
1590
1590
  .setting-row .card-actions, .module-row .card-actions { margin-top: 0; flex-wrap: wrap; justify-content: flex-end; }
1591
1591
  .module-row .card-actions > .record-card-edit { position: static; }
1592
- .character-card { cursor: pointer; container: character-card / inline-size; }
1592
+ .character-card { cursor: pointer; }
1593
1593
  .character-row { grid-template-columns: minmax(140px, .28fr) minmax(0, 1fr) auto; }
1594
1594
  .character-card-heading { display: flex; min-width: 0; align-items: center; gap: 7px; }
1595
1595
  .character-card-heading .character-avatar { width: 32px; height: 32px; font-size: 12px; }
1596
1596
  .character-card-heading h3 { flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; }
1597
- .record-card.has-card-edit .character-card-heading { padding-right: 70px; }
1597
+ .record-card.has-card-edit .character-card-heading { padding-right: 70px; transform: translateY(-3px); }
1598
1598
  .record-card.has-card-edit.has-pin-control .character-card-heading { padding-right: 106px; }
1599
1599
  .record-card.has-card-edit .character-card-heading h3 { padding-right: 0; }
1600
- .character-card.has-card-edit > .character-favorite-button, .character-card.has-card-edit > .record-card-edit { top: 21px; }
1600
+ .character-card.has-card-edit > .character-pin-button, .character-card.has-card-edit > .character-favorite-button, .character-card.has-card-edit > .record-card-edit { top: 18px; }
1601
1601
  .module-row .character-favorite-button { position: static; }
1602
1602
  .module-row .character-pin-button { position: static; }
1603
1603
  .module-row .record-favorite-button { position: static; }
1604
1604
  .module-row .record-pin-button { position: static; }
1605
- @container character-card (max-width: 320px) {
1606
- .record-card.has-card-edit.has-pin-control .character-card-heading { padding-top: 42px; padding-right: 0; }
1607
- }
1608
1605
  @media (max-width: 560px) {
1609
1606
  .character-card-heading h3 { font-size: 15px; }
1610
1607
  }
@@ -3042,8 +3039,15 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3042
3039
  animation: toast-appear-top .18s ease;
3043
3040
  pointer-events: auto;
3044
3041
  }
3045
- .toast:not(.toast-confirmation):not(.chapter-insight-toast) { cursor: pointer; }
3042
+ .toast:not(.toast-confirmation):not(.chapter-insight-toast):not(.import-progress-toast) { cursor: pointer; }
3046
3043
  .toast.ai-new-conversation-toast { white-space: pre-line; }
3044
+ .import-progress-region,
3045
+ .import-progress-region:popover-open { top: auto; right: 24px; bottom: 24px; left: auto; }
3046
+ .import-progress-toast { display: grid; gap: 8px; width: min(360px, calc(100vw - 48px)); cursor: progress; }
3047
+ .import-progress-copy { display: grid; gap: 3px; }
3048
+ .import-progress-copy strong { font-size: 12px; }
3049
+ .import-progress-status { color: color-mix(in srgb, var(--toast-fg) 78%, transparent); font-size: 10px; }
3050
+ .import-progress-toast progress { width: 100%; height: 6px; accent-color: var(--accent); }
3047
3051
  .toast-region[data-position="bottom-right"] .toast { animation-name: toast-appear-bottom; }
3048
3052
  .toast.error {
3049
3053
  border-color: color-mix(in srgb, var(--toast-error-fg) 16%, transparent);
@@ -4099,7 +4103,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
4099
4103
  .module-content { padding: 18px 0 56px; }
4100
4104
  .card-grid, .provider-card-grid { grid-template-columns: minmax(0, 1fr); }
4101
4105
  .record-card { padding: 15px; }
4102
- .character-card.has-card-edit > .character-favorite-button, .character-card.has-card-edit > .record-card-edit { top: 12px; }
4106
+ .character-card.has-card-edit > .character-pin-button, .character-card.has-card-edit > .character-favorite-button, .character-card.has-card-edit > .record-card-edit { top: 8px; }
4103
4107
  .card-actions, .task-row-actions, .module-header-actions { gap: 8px; }
4104
4108
  .card-actions button, .task-row-actions button { flex: 1 1 auto; min-height: 38px; }
4105
4109
  .setting-row, .module-row { padding: 13px; }
package/dist/store.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CHARACTER_GENDERS, DRAFT_SETTING_MODULES } from "./domain.js";
2
2
  import { createHash } from "node:crypto";
3
- import { ENTITY_VERSION_BASELINE_MIGRATION_VERSION, PLATFORM_AI_WORK_ID } from "./database.js";
3
+ import { ENTITY_VERSION_BASELINE_MIGRATION_VERSION, PLATFORM_AI_WORK_ID, SYSTEM_USER_ID } from "./database.js";
4
4
  import { exportWorkDocx } from "./docx-export.js";
5
5
  import { createEpubArchive } from "./epub-export.js";
6
6
  import { AppError, notFound } from "./errors.js";
@@ -800,11 +800,11 @@ export class Store {
800
800
  this.getWork(workId);
801
801
  if (type === "work") {
802
802
  return this.db.transaction(() => {
803
- const ownerUserId = typeof snapshot.ownerUserId === "string" ? snapshot.ownerUserId : null;
803
+ const ownerUserId = this.resolveWorkOwnerUserId(typeof snapshot.ownerUserId === "string" ? snapshot.ownerUserId : null, true);
804
804
  const timestamp = now();
805
805
  this.db.run(`INSERT INTO works (id, title, author, description, language, cover_url, tags_json, version_no, created_at, updated_at, owner_user_id)
806
806
  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 : []), timestamp, timestamp, ownerUserId);
807
- if (ownerUserId) {
807
+ if (ownerUserId !== SYSTEM_USER_ID) {
808
808
  this.db.run("INSERT INTO work_memberships (work_id, user_id, role, invited_by_user_id, created_at) VALUES (?, ?, 'owner', ?, ?)", entityId, ownerUserId, ownerUserId, timestamp);
809
809
  }
810
810
  const versionNo = this.recordEntityVersion("work", entityId, "restore", sourceRef, changeNote, timestamp);
@@ -863,21 +863,29 @@ export class Store {
863
863
  }
864
864
  }
865
865
  }
866
- createWork(input) {
866
+ createWork(input, ownerUserId = null) {
867
867
  const workId = id("work");
868
868
  const timestamp = now();
869
- const actor = currentRequestActor();
869
+ const resolvedOwnerUserId = this.resolveWorkOwnerUserId(ownerUserId);
870
870
  this.db.transaction(() => {
871
871
  this.db.run(`INSERT INTO works (id, title, author, description, language, cover_url, tags_json, created_at, updated_at, owner_user_id)
872
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, workId, input.title, input.author ?? "", input.description ?? "", input.language ?? "zh-CN", input.coverUrl ?? null, JSON.stringify(input.tags ?? []), timestamp, timestamp, actor?.userId ?? null);
873
- if (actor) {
874
- this.db.run("INSERT INTO work_memberships (work_id, user_id, role, invited_by_user_id, created_at) VALUES (?, ?, 'owner', ?, ?)", workId, actor.userId, actor.userId, timestamp);
872
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, workId, input.title, input.author ?? "", input.description ?? "", input.language ?? "zh-CN", input.coverUrl ?? null, JSON.stringify(input.tags ?? []), timestamp, timestamp, resolvedOwnerUserId);
873
+ if (resolvedOwnerUserId !== SYSTEM_USER_ID) {
874
+ this.db.run("INSERT INTO work_memberships (work_id, user_id, role, invited_by_user_id, created_at) VALUES (?, ?, 'owner', ?, ?)", workId, resolvedOwnerUserId, resolvedOwnerUserId, timestamp);
875
875
  }
876
876
  this.recordEntityVersion("work", workId, "create", null, "建立作品", timestamp);
877
877
  this.audit(workId, "work.created", "work", workId);
878
878
  });
879
879
  return this.getWork(workId);
880
880
  }
881
+ resolveWorkOwnerUserId(ownerUserId = null, allowUnknownFallback = false) {
882
+ const candidate = ownerUserId ?? currentRequestActor()?.userId ?? null;
883
+ if (candidate && this.db.get("SELECT 1 AS present FROM users WHERE id = ?", candidate))
884
+ return candidate;
885
+ if (candidate && !allowUnknownFallback)
886
+ throw new AppError(400, "WORK_OWNER_INVALID", "作品 Owner 用户不存在");
887
+ return SYSTEM_USER_ID;
888
+ }
881
889
  listWorks() {
882
890
  const actor = currentRequestActor();
883
891
  if (!actor) {
@@ -1702,9 +1710,9 @@ export class Store {
1702
1710
  this.db.transaction(() => { result = this.importNovelInTransaction(workId, fileName, fileType, parsed, mode, expectedVersionNo); });
1703
1711
  return { ...result, tree: this.getWorkDirectory(workId) };
1704
1712
  }
1705
- createImportedWork(input, fileName, fileType, parsed) {
1713
+ createImportedWork(input, fileName, fileType, parsed, ownerUserId = null) {
1706
1714
  return this.db.transaction(() => {
1707
- const work = this.createWork(input);
1715
+ const work = this.createWork(input, ownerUserId);
1708
1716
  const imported = this.importNovelInTransaction(String(work.id), fileName, fileType, parsed, undefined, undefined, false);
1709
1717
  return { ...imported, work: this.getWork(String(work.id)) };
1710
1718
  });