@musnows/scriverse 0.5.10 → 0.5.11

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.
@@ -1,6 +1,7 @@
1
1
  import { levelLabel, relationshipCategoryLabel } from "./display-labels.js?v=20260725-enum-labels-zh";
2
2
 
3
3
  export const VERSIONED_ENTITY_LABELS = Object.freeze({
4
+ draft: "创作草稿",
4
5
  setting: "世界观设定",
5
6
  race: "种族档案",
6
7
  organization: "组织档案",
@@ -24,6 +25,7 @@ export function entityVersionSourceLabel(source) {
24
25
  }
25
26
 
26
27
  export function entityVersionSnapshotSummary(type, snapshot = {}) {
28
+ if (type === "draft") return `${snapshot.draftType === "setting" ? "设定草稿" : "正文草稿"} · ${snapshot.title || "未命名草稿"}`;
27
29
  if (type === "setting") return `${snapshot.category || "未分类"} · ${snapshot.title || "未命名设定"}`;
28
30
  if (type === "race") return `${snapshot.name || "未命名种族"} · ${(snapshot.memberIds ?? []).length} 位角色`;
29
31
  if (type === "organization") return `${snapshot.name || "未命名组织"} · ${(snapshot.memberIds ?? []).length} 位成员`;
@@ -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=20260729-analysis-rerun-vditor-ui-v1">
13
+ <link rel="stylesheet" href="/styles.css?v=20260730-table-wrap-menu-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -123,6 +123,7 @@
123
123
  </div>
124
124
  <nav id="module-nav" class="module-nav" aria-label="作品模块">
125
125
  <button type="button" data-module="editor" class="active"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>正文</button>
126
+ <button type="button" data-module="drafts"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20h4l11-11a2.8 2.8 0 0 0-4-4L4 16v4Z"/><path d="m13.5 6.5 4 4"/></svg>草稿</button>
126
127
  <button type="button" data-module="settings"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m16 6 4 14"/><path d="M12 6v14"/><path d="M8 8v12"/><path d="M4 4v16"/></svg>设定库</button>
127
128
  <button type="button" data-module="characters"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>角色</button>
128
129
  <button type="button" data-module="organizations"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>组织</button>
@@ -333,6 +334,12 @@
333
334
  <button id="add-line-annotation" type="button" role="menuitem">添加批注</button>
334
335
  <button id="add-line-todo" type="button" role="menuitem">添加待办</button>
335
336
  </div>
337
+ <div id="markdown-table-menu" class="markdown-table-menu hidden" role="menu" aria-label="Markdown 表格显示方式">
338
+ <button id="markdown-table-wrap-toggle" type="button" role="menuitemcheckbox" aria-checked="false">
339
+ <span>表格内容自动换行</span>
340
+ <small>关闭时可横向滚动</small>
341
+ </button>
342
+ </div>
336
343
  <input id="new-import-file" class="hidden" type="file" accept=".txt,.docx">
337
344
  <input id="cover-file" class="hidden" type="file" accept="image/png,image/jpeg,image/webp">
338
345
  <input id="avatar-file" class="hidden" type="file" accept="image/png,image/jpeg,image/webp">
@@ -713,6 +720,7 @@
713
720
  <legend>可分页模块单页数量</legend>
714
721
  <small>以下模块独立设置,允许 10–100 条;默认均为 30 条。种族层级始终完整展示。</small>
715
722
  <div class="pagination-settings-grid">
723
+ <label>草稿列表<input id="page-size-drafts" name="pageSizeDrafts" type="number" min="10" max="100" step="1" required aria-label="草稿列表单页数量"></label>
716
724
  <label>设定库<input id="page-size-settings" name="pageSizeSettings" type="number" min="10" max="100" step="1" required aria-label="设定库单页数量"></label>
717
725
  <label>角色列表<input id="page-size-characters" name="pageSizeCharacters" type="number" min="10" max="100" step="1" required aria-label="角色列表单页数量"></label>
718
726
  <label>组织列表<input id="page-size-organizations" name="pageSizeOrganizations" type="number" min="10" max="100" step="1" required aria-label="组织列表单页数量"></label>
@@ -876,6 +884,6 @@
876
884
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
877
885
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
878
886
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
879
- <script type="module" src="/app.js?v=20260729-analysis-rerun-vditor-ui-v1"></script>
887
+ <script type="module" src="/app.js?v=20260730-table-wrap-menu-v1"></script>
880
888
  </body>
881
889
  </html>
@@ -128,7 +128,12 @@ function tableAlignments(cells) {
128
128
  }
129
129
 
130
130
  function renderMarkdownTable(headers, alignments, rows) {
131
- const renderCell = (tag, value, alignment) => `<${tag} class="markdown-align-${alignment}">${renderInlineMarkdown(value)}</${tag}>`;
131
+ const renderCell = (tag, value, alignment) => {
132
+ const interaction = tag === "th"
133
+ ? ' data-markdown-table-header tabindex="0" aria-haspopup="menu" title="右键或按 Shift+F10 设置表格换行"'
134
+ : "";
135
+ return `<${tag} class="markdown-align-${alignment}"${interaction}>${renderInlineMarkdown(value)}</${tag}>`;
136
+ };
132
137
  const header = headers.map((cell, index) => renderCell("th", cell, alignments[index])).join("");
133
138
  const body = rows.map((row) => {
134
139
  const cells = headers.map((_header, index) => renderCell("td", row[index] ?? "", alignments[index])).join("");
@@ -1,4 +1,4 @@
1
- export type RestorableModule = "settings" | "characters" | "races" | "organizations" | "timeline" | "outlines" | "relationships" | "reviews" | "tasks" | "ai-settings";
1
+ export type RestorableModule = "drafts" | "settings" | "characters" | "races" | "organizations" | "timeline" | "outlines" | "relationships" | "reviews" | "tasks" | "ai-settings";
2
2
  export type PageRoute =
3
3
  | { view: "shelf" }
4
4
  | { view: "editor"; workId: string; chapterId: string | null }
@@ -1,4 +1,5 @@
1
1
  export const RESTORABLE_MODULES = Object.freeze([
2
+ "drafts",
2
3
  "settings",
3
4
  "characters",
4
5
  "races",
@@ -0,0 +1,19 @@
1
+ export type StreamTypewriterProgress = {
2
+ visibleCharacters: number;
3
+ pendingCharacters: number;
4
+ };
5
+
6
+ export type StreamTypewriter = {
7
+ append(value: unknown): void;
8
+ finish(): Promise<string>;
9
+ reveal(): string;
10
+ };
11
+
12
+ export function streamTypewriterBatchSize(pendingCharacters: number, finishingFrames?: number | null): number;
13
+
14
+ export function createStreamTypewriter<FrameHandle = number>(options: {
15
+ onRender: (text: string, progress: StreamTypewriterProgress) => void;
16
+ scheduleFrame?: (callback: () => void) => FrameHandle;
17
+ cancelFrame?: (handle: FrameHandle) => void;
18
+ reducedMotion?: boolean;
19
+ }): StreamTypewriter;
@@ -0,0 +1,79 @@
1
+ const DEFAULT_FINISH_FRAMES = 36;
2
+ const DEFAULT_BACKLOG_FRAMES = 18;
3
+
4
+ export function streamTypewriterBatchSize(pendingCharacters, finishingFrames = null) {
5
+ const pending = Math.max(0, Math.floor(Number(pendingCharacters) || 0));
6
+ if (pending === 0) return 0;
7
+ const targetFrames = finishingFrames === null
8
+ ? DEFAULT_BACKLOG_FRAMES
9
+ : Math.max(1, Math.floor(Number(finishingFrames) || 1));
10
+ return Math.max(1, Math.ceil(pending / targetFrames));
11
+ }
12
+
13
+ export function createStreamTypewriter({
14
+ onRender,
15
+ scheduleFrame = (callback) => window.requestAnimationFrame(callback),
16
+ cancelFrame = (handle) => window.cancelAnimationFrame(handle),
17
+ reducedMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true
18
+ }) {
19
+ if (typeof onRender !== "function") throw new TypeError("onRender must be a function");
20
+
21
+ const visibleCharacters = [];
22
+ const pendingCharacters = [];
23
+ const idleResolvers = [];
24
+ let scheduledFrame = null;
25
+ let finishingFrames = null;
26
+
27
+ const snapshot = () => visibleCharacters.join("");
28
+ const resolveIdle = () => {
29
+ if (pendingCharacters.length || scheduledFrame !== null) return;
30
+ const value = snapshot();
31
+ for (const resolve of idleResolvers.splice(0)) resolve(value);
32
+ };
33
+ const render = () => {
34
+ onRender(snapshot(), {
35
+ visibleCharacters: visibleCharacters.length,
36
+ pendingCharacters: pendingCharacters.length
37
+ });
38
+ };
39
+ const schedule = () => {
40
+ if (scheduledFrame !== null || pendingCharacters.length === 0) return;
41
+ scheduledFrame = scheduleFrame(() => {
42
+ scheduledFrame = null;
43
+ const batchSize = reducedMotion
44
+ ? pendingCharacters.length
45
+ : streamTypewriterBatchSize(pendingCharacters.length, finishingFrames);
46
+ visibleCharacters.push(...pendingCharacters.splice(0, batchSize));
47
+ if (finishingFrames !== null) finishingFrames = Math.max(1, finishingFrames - 1);
48
+ render();
49
+ if (pendingCharacters.length) schedule();
50
+ else resolveIdle();
51
+ });
52
+ };
53
+
54
+ return {
55
+ append(value) {
56
+ const characters = Array.from(String(value ?? ""));
57
+ if (!characters.length) return;
58
+ pendingCharacters.push(...characters);
59
+ schedule();
60
+ },
61
+ finish() {
62
+ if (!pendingCharacters.length && scheduledFrame === null) return Promise.resolve(snapshot());
63
+ finishingFrames = reducedMotion ? 1 : DEFAULT_FINISH_FRAMES;
64
+ schedule();
65
+ return new Promise((resolve) => idleResolvers.push(resolve));
66
+ },
67
+ reveal() {
68
+ if (scheduledFrame !== null) {
69
+ cancelFrame(scheduledFrame);
70
+ scheduledFrame = null;
71
+ }
72
+ visibleCharacters.push(...pendingCharacters.splice(0));
73
+ finishingFrames = null;
74
+ render();
75
+ resolveIdle();
76
+ return snapshot();
77
+ }
78
+ };
79
+ }
@@ -1275,6 +1275,10 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1275
1275
  .character-filter-toolbar-actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px; min-height: 38px; }
1276
1276
  .character-filter-result-count { color: var(--muted); font-size: 11px; white-space: nowrap; }
1277
1277
  .character-filter-toolbar-actions button:disabled { cursor: default; opacity: .45; }
1278
+ .draft-filter-toolbar { display: flex; align-items: end; gap: 10px; margin-bottom: 18px; padding: 12px 14px; border: 1px solid var(--line); background: var(--surface-soft); }
1279
+ .draft-filter-toolbar label { color: var(--muted); font-size: 11px; }
1280
+ .draft-filter-toolbar select { min-width: 180px; padding: 8px 10px; background: var(--surface); }
1281
+ .draft-filter-toolbar span { margin-left: auto; align-self: center; color: var(--muted); font-size: 11px; }
1278
1282
  .character-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
1279
1283
  .character-duplicate-review { grid-column: 1 / -1; }
1280
1284
  .character-duplicate-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 14px 0; }
@@ -1343,10 +1347,11 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1343
1347
  border-radius: 5px;
1344
1348
  background: var(--surface-soft);
1345
1349
  }
1346
- .task-auto-run-copy { display: grid; grid-column: 1; gap: 4px; }
1350
+ .task-auto-run-header { display: grid; grid-column: 1; grid-template-columns: minmax(0, 1fr) auto; align-items: start; gap: 12px; }
1351
+ .task-auto-run-copy { display: grid; gap: 4px; }
1347
1352
  .task-auto-run-copy strong { font-size: 13px; }
1348
1353
  .task-auto-run-copy small { color: var(--muted); font-size: 10px; line-height: 1.5; }
1349
- .task-auto-run-actions { display: flex; grid-column: 2; grid-row: 1; align-items: start; justify-content: flex-end; min-height: 32px; }
1354
+ .task-auto-run-actions { display: flex; align-items: start; justify-content: flex-end; min-height: 32px; }
1350
1355
  .task-auto-run-actions:empty { display: none; }
1351
1356
  .task-auto-run-controls {
1352
1357
  display: flex;
@@ -1897,6 +1902,40 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1897
1902
  .outline-summary strong { font-size: 26px; }.outline-summary span { color: var(--muted); font-size: 10px; }
1898
1903
  .planning-section + .planning-section { margin-top: 42px; }.section-title { display: flex; justify-content: space-between; margin-bottom: 15px; }.section-title h2 { margin: 0; font-size: 21px; }
1899
1904
  .foreshadow-card.is-overdue { border-color: #b55743; box-shadow: inset 3px 0 #b55743; }.foreshadow-links { margin-top: 13px; }
1905
+ .foreshadow-editor-section { display: grid; gap: 14px; padding: 18px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
1906
+ .foreshadow-editor-section > header, .foreshadow-occurrence-toolbar > div { display: grid; gap: 4px; }
1907
+ .foreshadow-editor-section h3 { margin: 0; color: var(--ink); font-size: 14px; font-weight: 650; }
1908
+ .foreshadow-editor-section header p, .foreshadow-occurrence-toolbar p { margin: 0; color: var(--muted); font-size: 10px; line-height: 1.5; }
1909
+ .foreshadow-overview-grid { display: grid; grid-template-columns: minmax(220px, 1.5fr) repeat(2, minmax(130px, .65fr)) minmax(220px, 1.2fr); gap: 12px; }
1910
+ .foreshadow-overview-description textarea, .foreshadow-resolution-field textarea { min-height: 108px; }
1911
+ .foreshadow-occurrence-field { gap: 16px; }
1912
+ .foreshadow-occurrence-toolbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; }
1913
+ .foreshadow-occurrence-toolbar button, .foreshadow-occurrence-remove { flex: 0 0 auto; border: 1px solid var(--line); background: var(--surface); color: var(--ink); }
1914
+ .foreshadow-occurrence-rows { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; align-items: start; }
1915
+ .foreshadow-occurrence-row { --occurrence-color: var(--accent); position: relative; min-width: 0; display: grid; gap: 12px; margin: 0; padding: 14px 14px 15px; border: 1px solid color-mix(in srgb, var(--occurrence-color) 35%, var(--line)); border-left: 4px solid var(--occurrence-color); border-radius: 6px; background: color-mix(in srgb, var(--occurrence-color) 5%, var(--surface)); }
1916
+ .foreshadow-occurrence-row[data-occurrence-role="setup"] { --occurrence-color: #9a6547; }
1917
+ .foreshadow-occurrence-row[data-occurrence-role="reminder"] { --occurrence-color: #80713d; }
1918
+ .foreshadow-occurrence-row[data-occurrence-role="payoff"] { --occurrence-color: #49745c; }
1919
+ .foreshadow-occurrence-row legend { display: flex; max-width: calc(100% - 94px); align-items: center; gap: 7px; padding: 0 7px; color: var(--ink); }
1920
+ .foreshadow-occurrence-row legend strong { font-size: 11px; }
1921
+ .foreshadow-occurrence-row legend small { min-width: 0; overflow: hidden; color: var(--muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
1922
+ .foreshadow-occurrence-role-badge { flex: 0 0 auto; padding: 3px 7px; border: 1px solid color-mix(in srgb, var(--occurrence-color) 55%, var(--line)); border-radius: 12px; background: color-mix(in srgb, var(--occurrence-color) 12%, var(--surface)); color: var(--occurrence-color); font-size: 9px; font-weight: 700; }
1923
+ .foreshadow-occurrence-remove { position: absolute; top: 9px; right: 12px; padding: 5px 8px; font-size: 10px; }
1924
+ .foreshadow-occurrence-grid { display: grid; grid-template-columns: minmax(110px, .55fr) minmax(180px, 1.45fr); gap: 10px; }
1925
+ .dialog-fields .foreshadow-occurrence-row textarea { min-height: 68px; }
1926
+ .foreshadow-occurrence-empty { margin: 0; padding: 12px; border: 1px dashed var(--line); color: var(--muted); font-size: 11px; text-align: center; }
1927
+ @media (max-width: 900px) {
1928
+ .foreshadow-overview-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1929
+ .foreshadow-occurrence-rows { grid-template-columns: minmax(0, 1fr); }
1930
+ }
1931
+ @media (max-width: 620px) {
1932
+ .foreshadow-occurrence-toolbar { align-items: stretch; flex-direction: column; }
1933
+ .foreshadow-occurrence-toolbar button { align-self: flex-start; }
1934
+ .foreshadow-overview-grid { grid-template-columns: minmax(0, 1fr); }
1935
+ .foreshadow-occurrence-grid { grid-template-columns: minmax(0, 1fr); }
1936
+ .foreshadow-occurrence-row legend { max-width: calc(100% - 78px); flex-wrap: wrap; }
1937
+ .foreshadow-occurrence-row legend small { flex-basis: 100%; }
1938
+ }
1900
1939
  .outline-list { display: grid; gap: 8px; }.outline-row { display: grid; grid-template-columns: minmax(150px, 1.1fr) repeat(3, minmax(130px, 1fr)) auto; gap: 16px; align-items: start; padding: 15px; border: 1px solid var(--line); background: var(--surface-soft); }
1901
1940
  .outline-row small { color: var(--accent); font-size: 9px; }.outline-row h3 { margin: 4px 0 0; font-size: 14px; }.outline-row b { font-size: 10px; }.outline-row p { margin: 5px 0 0; color: var(--muted); font-size: 10px; line-height: 1.45; white-space: pre-wrap; }
1902
1941
  .outline-row.is-complete { opacity: .66; }.outline-actions { display: grid; gap: 7px; justify-items: end; }.outline-actions span { color: var(--accent); font-size: 9px; }.outline-actions button { border: 1px solid var(--line); background: transparent; border-radius: 3px; padding: 5px 8px; font-size: 9px; }
@@ -1999,6 +2038,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1999
2038
  .assistant-message > .message-heading, .user-message > .message-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 5px; opacity: .65; font-size: 9px; }
2000
2039
  .message-heading > span { letter-spacing: .12em; }.message-heading time { flex: 0 0 auto; font-variant-numeric: tabular-nums; letter-spacing: .03em; }
2001
2040
  .message-body { min-width: 0; overflow-wrap: anywhere; }
2041
+ .is-streaming .message-body:empty::after, .is-streaming .message-body > :last-child::after { content: ""; display: inline-block; width: 2px; height: 1.05em; margin-left: 3px; border-radius: 1px; background: var(--accent-dark); vertical-align: -.16em; animation: ai-stream-cursor 850ms steps(1, end) infinite; }
2002
2042
  .message-card-actions { position: absolute; right: 0; bottom: -31px; display: flex; align-items: center; gap: 12px; height: 24px; }
2003
2043
  .message-card-actions button { display: inline-flex; align-items: center; gap: 4px; min-width: 0; padding: 3px 2px; border: 0; background: transparent; color: var(--muted); font-size: 9px; }
2004
2044
  .message-card-actions button:hover, .message-card-actions button:focus-visible { color: var(--accent-dark); }
@@ -2017,6 +2057,11 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2017
2057
  .message-body .markdown-table-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
2018
2058
  .message-body .markdown-table-scroll table { width: max-content; min-width: 100%; border-collapse: collapse; }
2019
2059
  .message-body .markdown-table-scroll th, .message-body .markdown-table-scroll td { padding: 7px 9px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); overflow-wrap: normal; white-space: nowrap; }
2060
+ .message-body .markdown-table-scroll th { cursor: context-menu; }
2061
+ .message-body .markdown-table-scroll th:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
2062
+ .message-body .markdown-table-scroll.is-wrapping { overflow-x: hidden; scrollbar-gutter: auto; }
2063
+ .message-body .markdown-table-scroll.is-wrapping table { width: 100%; table-layout: fixed; }
2064
+ .message-body .markdown-table-scroll.is-wrapping th, .message-body .markdown-table-scroll.is-wrapping td { overflow-wrap: anywhere; white-space: normal; word-break: break-word; }
2020
2065
  .message-body .markdown-table-scroll th:last-child, .message-body .markdown-table-scroll td:last-child { border-right: 0; }
2021
2066
  .message-body .markdown-table-scroll tbody tr:last-child td { border-bottom: 0; }
2022
2067
  .message-body .markdown-table-scroll th { background: var(--surface-soft); font-weight: 600; }
@@ -2042,7 +2087,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2042
2087
  .ai-process-intermediate-step .ai-process-step-body { border-left-color: var(--muted); }
2043
2088
  .ai-process-tool-step { padding-top: 1px; }
2044
2089
  @keyframes ai-process-pulse { 0%, 100% { opacity: .45; transform: scale(.8); } 50% { opacity: 1; transform: scale(1); } }
2045
- @media (prefers-reduced-motion: reduce) { .is-streaming .ai-process-details[open] > summary::before { animation: none; } }
2090
+ @keyframes ai-stream-cursor { 0%, 54% { opacity: 1; } 55%, 100% { opacity: 0; } }
2091
+ @media (prefers-reduced-motion: reduce) { .is-streaming .ai-process-details[open] > summary::before, .is-streaming .message-body:empty::after, .is-streaming .message-body > :last-child::after { animation: none; } }
2046
2092
  .ai-tool-call-list { display: grid; gap: 5px; margin-top: 10px; }
2047
2093
  .ai-tool-call-summary { display: flex; align-items: center; justify-content: space-between; width: 100%; min-width: 0; padding: 7px 9px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface-soft); color: var(--muted); font: 9px/1.4 var(--font-latin), monospace; text-align: left; }
2048
2094
  .ai-tool-call-summary::after { content: "查看详情"; flex: 0 0 auto; margin-left: 8px; color: var(--accent-dark); }
@@ -2203,6 +2249,13 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2203
2249
  .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); }
2204
2250
  .line-citation-menu strong { padding: 5px 7px 8px; color: var(--muted); font-size: 9px; }
2205
2251
  .line-citation-menu button { border: 0; border-radius: 4px; padding: 8px; background: transparent; text-align: left; font-size: 11px; }.line-citation-menu button:hover { background: var(--paper-deep); color: var(--accent-dark); }
2252
+ .markdown-table-menu { position: fixed; z-index: 84; min-width: 210px; padding: 7px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); box-shadow: 0 14px 38px rgba(49,42,32,.22); }
2253
+ .markdown-table-menu button { position: relative; display: grid; width: 100%; gap: 2px; padding: 8px 9px 8px 31px; border: 0; border-radius: 4px; background: transparent; color: var(--ink); text-align: left; }
2254
+ .markdown-table-menu button::before { content: ""; position: absolute; top: 12px; left: 10px; width: 11px; height: 11px; border: 1px solid var(--muted); border-radius: 2px; background: var(--surface); }
2255
+ .markdown-table-menu button[aria-checked="true"]::before { border-color: var(--accent); background: var(--accent); box-shadow: inset 0 0 0 3px var(--surface); }
2256
+ .markdown-table-menu button:hover, .markdown-table-menu button:focus-visible { background: var(--paper-deep); color: var(--accent-dark); }
2257
+ .markdown-table-menu button span { font-size: 11px; font-weight: 600; }
2258
+ .markdown-table-menu button small { color: var(--muted); font-size: 9px; }
2206
2259
  .chapter-annotations-dialog { width: min(820px, 94vw); }
2207
2260
  .chapter-annotations-list { display: grid; gap: 10px; max-height: 62vh; overflow: auto; padding: 18px 24px; }
2208
2261
  .chapter-annotation-card { display: grid; gap: 8px; padding: 13px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
@@ -2223,6 +2276,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2223
2276
  .wide-dialog { width: min(800px, 92vw); }
2224
2277
  .trace-dialog { width: min(1180px, 94vw); }
2225
2278
  .trace-dialog .dialog-fields { max-height: 68vh; }
2279
+ .large-dialog { width: min(1120px, 94vw); max-height: 94dvh; }
2280
+ .large-dialog .dialog-fields { max-height: 72dvh; padding: 24px 28px; }
2226
2281
  .dialog::backdrop { background: rgba(34,30,25,.42); backdrop-filter: blur(3px); }
2227
2282
  .dialog-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 22px 24px 16px; border-bottom: 1px solid var(--line); }
2228
2283
  .dialog-header h2 { margin: 0; font-size: 23px; font-weight: 500; }
@@ -2247,6 +2302,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2247
2302
  outline: none;
2248
2303
  }
2249
2304
  .dialog-fields { display: grid; gap: 14px; padding: 22px 24px; max-height: 58vh; overflow-y: auto; }
2305
+ .dialog-fields .form-field-note { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.65; }
2250
2306
  .dialog-fields label { display: grid; gap: 6px; color: var(--muted); font-size: 11px; }
2251
2307
  .dialog-fields input, .dialog-fields textarea, .dialog-fields select { width: 100%; padding: 9px 10px; font-size: 13px; }
2252
2308
  .dialog-fields textarea { resize: vertical; min-height: 90px; line-height: 1.6; }
@@ -2444,6 +2500,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2444
2500
  .import-mode-dialog .dialog-actions { flex-wrap: wrap; }
2445
2501
  .character-filter-toolbar { grid-template-columns: minmax(0, 1fr); }
2446
2502
  .character-filter-toolbar-actions { justify-content: flex-start; }
2503
+ .draft-filter-toolbar { align-items: stretch; flex-direction: column; }
2504
+ .draft-filter-toolbar select { width: 100%; min-width: 0; }
2505
+ .draft-filter-toolbar span { margin-left: 0; }
2447
2506
  }
2448
2507
  .entity-editor-open { overflow: hidden; }
2449
2508
  .preview-record-card { cursor: pointer; }
@@ -2494,6 +2553,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2494
2553
  .setting-markdown-heading, .markdown-editor-field-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 24px; color: var(--ink); font-size: 14px; }
2495
2554
  .setting-markdown-heading small, .markdown-editor-field-heading small { color: var(--muted); font-size: 10px; }
2496
2555
  .markdown-editor-field { display: grid; gap: 10px; min-width: 0; }
2556
+ .markdown-editor-field > span { color: var(--muted); font-size: 11px; }
2497
2557
  .markdown-editor-field .vditor-editor-host { min-height: 360px; }
2498
2558
  .vditor-editor-host { width: 100%; height: 100%; min-width: 0; min-height: 0; }
2499
2559
  .vditor-editor-host.vditor { height: 100% !important; min-height: 0; overflow: hidden; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); }
@@ -2730,8 +2790,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2730
2790
  .top-actions .ghost-button:not(.settings-button) { display: none; }
2731
2791
  .book-shelf { grid-template-columns: repeat(3, minmax(180px, 1fr)); }
2732
2792
  .task-auto-run-panel { grid-template-columns: minmax(0, 1fr); column-gap: 0; }
2733
- .task-auto-run-copy, .task-auto-run-actions, .task-auto-run-controls, .task-auto-run-meta { grid-column: 1; }
2734
- .task-auto-run-actions { grid-row: auto; }
2793
+ .task-auto-run-header, .task-auto-run-controls, .task-auto-run-meta { grid-column: 1; }
2735
2794
  .task-auto-run-help, .task-auto-run-alert { grid-column: 1; }
2736
2795
  .task-auto-run-progress { grid-column: 1; grid-row: auto; justify-self: stretch; width: 100%; }
2737
2796
  .task-auto-run-progress-ring { display: none; }
@@ -2970,7 +3029,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2970
3029
  .settings-layout-toggle, .module-layout-toggle { width: 100%; }
2971
3030
  .settings-layout-toggle button { flex: 1; }
2972
3031
 
2973
- .dialog, .wide-dialog, .trace-dialog, .account-dialog {
3032
+ .dialog, .wide-dialog, .trace-dialog, .large-dialog, .account-dialog {
2974
3033
  width: calc(100vw - 20px);
2975
3034
  max-width: calc(100vw - 20px);
2976
3035
  max-height: calc(100dvh - 20px);
@@ -1,5 +1,5 @@
1
- export type WorkPermissionModule = "prose" | "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews" | "ai-chat" | "ai-analysis" | "ai-settings";
2
- export type WorkUiModule = "editor" | "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews" | "tasks" | "ai-settings";
1
+ export type WorkPermissionModule = "prose" | "drafts" | "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews" | "ai-chat" | "ai-analysis" | "ai-settings";
2
+ export type WorkUiModule = "editor" | "drafts" | "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews" | "tasks" | "ai-settings";
3
3
  export type WorkModuleAccess = "none" | "read" | "write";
4
4
  export type WorkModulePermissions = Record<WorkPermissionModule, WorkModuleAccess>;
5
5
  export type WorkPermissionAware = { accessRole?: string | null; modulePermissions?: WorkModulePermissions };
@@ -1,5 +1,6 @@
1
1
  export const WORK_PERMISSION_MODULES = Object.freeze([
2
2
  { id: "prose", uiModule: "editor", label: "正文" },
3
+ { id: "drafts", uiModule: "drafts", label: "草稿" },
3
4
  { id: "settings", uiModule: "settings", label: "设定库" },
4
5
  { id: "characters", uiModule: "characters", label: "角色" },
5
6
  { id: "races", uiModule: "races", label: "种族" },
@@ -21,6 +22,13 @@ const validAccess = new Set(["none", "read", "write"]);
21
22
  function migrateLegacyModulePermissions(value) {
22
23
  if (!value || typeof value !== "object" || Array.isArray(value)) return value;
23
24
  const migrated = { ...value };
25
+ if (!validAccess.has(migrated.drafts)) {
26
+ const prose = validAccess.has(migrated.prose) ? migrated.prose : "none";
27
+ const settings = validAccess.has(migrated.settings) ? migrated.settings : "none";
28
+ migrated.drafts = prose === "write" && settings === "write"
29
+ ? "write"
30
+ : prose === "none" || settings === "none" ? "none" : "read";
31
+ }
24
32
  const legacyAi = validAccess.has(migrated.ai) ? migrated.ai : null;
25
33
  if (!validAccess.has(migrated["ai-chat"]) && legacyAi) migrated["ai-chat"] = legacyAi;
26
34
  if (!validAccess.has(migrated["ai-analysis"]) && validAccess.has(migrated.ai)) {
package/dist/store.js CHANGED
@@ -7,6 +7,7 @@ import { currentRequestActor } from "./request-context.js";
7
7
  import { classifyWorkModulePermissions, emptyWorkModulePermissions, fullWorkModulePermissions, storedWorkModulePermissions } from "./work-permissions.js";
8
8
  import { countWords, documentShortSearchTerms, id, json, normalizeDocumentSearchText, normalizeParagraphSpacing, now, splitDocumentParagraphs } from "./utils.js";
9
9
  const defaultPlatformPageSizes = {
10
+ drafts: 30,
10
11
  settings: 30,
11
12
  characters: 30,
12
13
  races: 30,
@@ -29,6 +30,7 @@ function platformPageSizes(value) {
29
30
  : defaultPlatformPageSizes[key];
30
31
  };
31
32
  return {
33
+ drafts: pageSize("drafts"),
32
34
  settings: pageSize("settings"),
33
35
  characters: pageSize("characters"),
34
36
  races: pageSize("races"),
@@ -110,6 +112,7 @@ function settingsFromKnowledgeSections(sections) {
110
112
  export const versionedEntityTypes = [
111
113
  "work",
112
114
  "volume",
115
+ "draft",
113
116
  "setting",
114
117
  "race",
115
118
  "organization",
@@ -237,6 +240,8 @@ export class Store {
237
240
  return this.getWork(entityId);
238
241
  if (type === "volume")
239
242
  return this.getVolume(entityId);
243
+ if (type === "draft")
244
+ return this.getDraft(entityId);
240
245
  if (type === "setting")
241
246
  return this.getSetting(entityId);
242
247
  if (type === "race")
@@ -287,6 +292,12 @@ export class Store {
287
292
  keywords: entity.keywords,
288
293
  sortOrder: entity.sortOrder
289
294
  };
295
+ if (type === "draft")
296
+ return {
297
+ draftType: entity.draftType,
298
+ title: entity.title,
299
+ content: entity.content
300
+ };
290
301
  if (type === "setting")
291
302
  return {
292
303
  title: entity.title,
@@ -393,6 +404,7 @@ export class Store {
393
404
  const entities = [
394
405
  ...this.db.all("SELECT id, updated_at FROM works").map((row) => ["work", requiredString(row, "id"), requiredString(row, "updated_at")]),
395
406
  ...this.db.all("SELECT id, updated_at FROM volumes").map((row) => ["volume", requiredString(row, "id"), requiredString(row, "updated_at")]),
407
+ ...this.db.all("SELECT id, updated_at FROM drafts").map((row) => ["draft", requiredString(row, "id"), requiredString(row, "updated_at")]),
396
408
  ...this.db.all("SELECT id, updated_at FROM settings").map((row) => ["setting", requiredString(row, "id"), requiredString(row, "updated_at")]),
397
409
  ...this.db.all("SELECT id, updated_at FROM races").map((row) => ["race", requiredString(row, "id"), requiredString(row, "updated_at")]),
398
410
  ...this.db.all("SELECT id, updated_at FROM organizations").map((row) => ["organization", requiredString(row, "id"), requiredString(row, "updated_at")]),
@@ -474,6 +486,8 @@ export class Store {
474
486
  restored = this.updateWork(entityId, snapshot, expectedVersionNo, "restore", sourceRef, changeNote);
475
487
  else if (type === "volume")
476
488
  restored = this.updateVolume(entityId, snapshot, expectedVersionNo, "restore", sourceRef, changeNote);
489
+ else if (type === "draft")
490
+ restored = this.updateDraft(entityId, snapshot, "restore", sourceRef, changeNote, expectedVersionNo);
477
491
  else if (type === "setting")
478
492
  restored = this.updateSetting(entityId, snapshot, "restore", sourceRef, changeNote, expectedVersionNo);
479
493
  else if (type === "race")
@@ -513,6 +527,9 @@ export class Store {
513
527
  if (type === "volume") {
514
528
  return this.db.transaction(() => this.insertVolumeWithId(workId, entityId, snapshot, "restore", sourceRef, changeNote));
515
529
  }
530
+ if (type === "draft") {
531
+ return this.insertDraftWithId(workId, entityId, snapshot, "restore", sourceRef, changeNote);
532
+ }
516
533
  if (type === "setting") {
517
534
  return this.insertSettingWithId(workId, entityId, snapshot, "restore", sourceRef, changeNote);
518
535
  }
@@ -668,7 +685,7 @@ export class Store {
668
685
  autoRunConsecutiveFailures: Math.max(0, Number(row?.auto_run_consecutive_failures ?? 0) || 0),
669
686
  bookSummaryContextPercent: Math.min(90, Math.max(1, Number(row?.book_summary_context_percent ?? 50) || 50)),
670
687
  contextCompactThreshold: Math.min(90, Math.max(50, Number(row?.context_compact_threshold ?? 85) || 85)),
671
- agentTools: json(String(row?.agent_tools_json ?? '["story_index","read_chapters","search_story_entities","grep","read_character_sections"]'), ["story_index", "read_chapters", "search_story_entities", "grep", "read_character_sections"])
688
+ agentTools: json(String(row?.agent_tools_json ?? '["story_index","read_chapters","search_story_entities","grep","read_character_sections","search_drafts"]'), ["story_index", "read_chapters", "search_story_entities", "grep", "read_character_sections", "search_drafts"])
672
689
  .map((tool) => tool === "query_story_knowledge" ? "search_story_entities" : tool)
673
690
  .filter((tool, index, tools) => tools.indexOf(tool) === index),
674
691
  updatedAt: String(row?.updated_at ?? "")
@@ -2163,6 +2180,94 @@ export class Store {
2163
2180
  FROM chapters c JOIN volumes v ON v.id = c.volume_id WHERE c.id = ? AND c.work_id = ?`, chapterId, workId);
2164
2181
  return row ? numberValue(row, "sequence") : Number.MAX_SAFE_INTEGER;
2165
2182
  }
2183
+ createDraft(workId, input, source = "create", sourceRef = null) {
2184
+ this.getWork(workId);
2185
+ return this.insertDraftWithId(workId, id("draft"), input, source, sourceRef);
2186
+ }
2187
+ insertDraftWithId(workId, draftId, input, source = "create", sourceRef = null, changeNote = "") {
2188
+ const timestamp = now();
2189
+ this.db.transaction(() => {
2190
+ this.db.run(`INSERT INTO drafts (id, work_id, draft_type, title, content, created_at, updated_at)
2191
+ VALUES (?, ?, ?, ?, ?, ?, ?)`, draftId, workId, input.draftType, input.title, input.content, timestamp, timestamp);
2192
+ this.syncMarkdownAttachmentReferences(workId, "draft", draftId, input.content);
2193
+ this.recordEntityVersion("draft", draftId, source, sourceRef, changeNote || "建立创作草稿", timestamp);
2194
+ this.audit(workId, source === "restore" ? "draft.restored" : "draft.created", "draft", draftId, {
2195
+ draftType: input.draftType,
2196
+ source,
2197
+ sourceRef
2198
+ });
2199
+ });
2200
+ return this.getDraft(draftId);
2201
+ }
2202
+ listDrafts(workId, draftType, includeContent = false) {
2203
+ this.getWork(workId);
2204
+ return this.db.all(`SELECT * FROM drafts WHERE work_id = ? AND (? IS NULL OR draft_type = ?)
2205
+ ORDER BY updated_at DESC, title`, workId, draftType ?? null, draftType ?? null).map((row) => this.mapDraft(row, includeContent));
2206
+ }
2207
+ listDraftsPage(workId, pagination, draftType, includeContent = false) {
2208
+ this.getWork(workId);
2209
+ const page = paginationSql(pagination);
2210
+ const rows = this.db.all(`SELECT * FROM drafts WHERE work_id = ? AND (? IS NULL OR draft_type = ?)
2211
+ ORDER BY updated_at DESC, title${page.sql}`, workId, draftType ?? null, draftType ?? null, ...page.params);
2212
+ return paginated(rows.map((row) => this.mapDraft(row, includeContent)), pagination);
2213
+ }
2214
+ searchDrafts(workId, query, draftType, limit = 20) {
2215
+ this.getWork(workId);
2216
+ const safeLimit = Math.min(30, Math.max(1, Math.trunc(limit)));
2217
+ const normalizedQuery = query.normalize("NFKC").trim();
2218
+ const escapedQuery = normalizedQuery.replace(/[\\%_]/gu, "\\$&");
2219
+ const pattern = `%${escapedQuery}%`;
2220
+ const rows = normalizedQuery
2221
+ ? this.db.all(`SELECT * FROM drafts
2222
+ WHERE work_id = ? AND (? IS NULL OR draft_type = ?)
2223
+ AND (title LIKE ? ESCAPE '\\' COLLATE NOCASE OR content LIKE ? ESCAPE '\\' COLLATE NOCASE)
2224
+ ORDER BY CASE WHEN title LIKE ? ESCAPE '\\' COLLATE NOCASE THEN 0 ELSE 1 END, updated_at DESC
2225
+ LIMIT ?`, workId, draftType ?? null, draftType ?? null, pattern, pattern, pattern, safeLimit)
2226
+ : this.db.all(`SELECT * FROM drafts WHERE work_id = ? AND (? IS NULL OR draft_type = ?)
2227
+ ORDER BY updated_at DESC, title LIMIT ?`, workId, draftType ?? null, draftType ?? null, safeLimit);
2228
+ return rows.map((row) => this.mapDraft(row, true));
2229
+ }
2230
+ getDraft(draftId) {
2231
+ const row = this.db.get("SELECT * FROM drafts WHERE id = ?", draftId);
2232
+ if (!row)
2233
+ throw notFound("草稿");
2234
+ return this.mapDraft(row, true);
2235
+ }
2236
+ updateDraft(draftId, input, source = "manual", sourceRef = null, changeNote = "", expectedVersionNo) {
2237
+ const current = this.getDraft(draftId);
2238
+ const content = input.content ?? String(current.content);
2239
+ this.db.transaction(() => {
2240
+ this.assertExpectedVersion("draft", draftId, expectedVersionNo, "草稿");
2241
+ this.db.run("UPDATE drafts SET draft_type = ?, title = ?, content = ?, updated_at = ? WHERE id = ?", input.draftType ?? String(current.draftType), input.title ?? String(current.title), content, now(), draftId);
2242
+ this.syncMarkdownAttachmentReferences(String(current.workId), "draft", draftId, content);
2243
+ this.recordEntityVersion("draft", draftId, source, sourceRef, changeNote || "更新创作草稿");
2244
+ this.audit(String(current.workId), "draft.updated", "draft", draftId, { fields: Object.keys(input), source, sourceRef });
2245
+ });
2246
+ return this.getDraft(draftId);
2247
+ }
2248
+ deleteDraft(draftId, expectedVersionNo) {
2249
+ const current = this.getDraft(draftId);
2250
+ this.db.transaction(() => {
2251
+ this.assertExpectedVersion("draft", draftId, expectedVersionNo, "草稿");
2252
+ this.recordEntityVersion("draft", draftId, "delete", null, "删除创作草稿");
2253
+ this.clearMarkdownAttachmentReferences("draft", draftId);
2254
+ this.db.run("DELETE FROM drafts WHERE id = ?", draftId);
2255
+ this.audit(String(current.workId), "draft.deleted", "draft", draftId);
2256
+ });
2257
+ }
2258
+ mapDraft(row, includeContent) {
2259
+ const content = requiredString(row, "content");
2260
+ return {
2261
+ id: requiredString(row, "id"),
2262
+ workId: requiredString(row, "work_id"),
2263
+ draftType: requiredString(row, "draft_type"),
2264
+ title: requiredString(row, "title"),
2265
+ ...(includeContent ? { content } : { contentPreview: content.replace(/\s+/gu, " ").trim().slice(0, 320) }),
2266
+ versionNo: this.currentEntityVersionNo("draft", requiredString(row, "id")),
2267
+ createdAt: requiredString(row, "created_at"),
2268
+ updatedAt: requiredString(row, "updated_at")
2269
+ };
2270
+ }
2166
2271
  createSetting(workId, input, source = "create", sourceRef = null) {
2167
2272
  this.getWork(workId);
2168
2273
  return this.insertSettingWithId(workId, id("setting"), input, source, sourceRef);
@@ -5445,9 +5550,10 @@ export class Store {
5445
5550
  exportWork(workId) {
5446
5551
  const tree = this.getWorkTree(workId);
5447
5552
  return {
5448
- schemaVersion: 7,
5553
+ schemaVersion: 8,
5449
5554
  exportedAt: now(),
5450
5555
  work: tree,
5556
+ drafts: this.listDrafts(workId, undefined, true),
5451
5557
  settings: this.listSettings(workId),
5452
5558
  characters: this.listCharacters(workId, true, true),
5453
5559
  races: this.listRaces(workId),