@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.
- package/dist/ai.js +57 -4
- package/dist/ai.js.map +1 -1
- package/dist/app.js +42 -3
- package/dist/app.js.map +1 -1
- package/dist/cli-contract.js +25 -0
- package/dist/cli-contract.js.map +1 -1
- package/dist/cli-core.js +3 -0
- package/dist/cli-core.js.map +1 -1
- package/dist/database.js +48 -1
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +397 -58
- package/dist/public/entity-version.js +2 -0
- package/dist/public/index.html +10 -2
- package/dist/public/markdown.js +6 -1
- package/dist/public/page-route.d.ts +1 -1
- package/dist/public/page-route.js +1 -0
- package/dist/public/stream-typewriter.d.ts +19 -0
- package/dist/public/stream-typewriter.js +79 -0
- package/dist/public/styles.css +65 -6
- package/dist/public/work-permissions.d.ts +2 -2
- package/dist/public/work-permissions.js +8 -0
- package/dist/store.js +108 -2
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +12 -5
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/work-permissions.js +9 -0
- package/dist/work-permissions.js.map +1 -1
- package/package.json +1 -1
|
@@ -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} 位成员`;
|
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="/vendor/vditor/dist/index.css?v=3.11.2">
|
|
13
|
-
<link rel="stylesheet" href="/styles.css?v=
|
|
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=
|
|
887
|
+
<script type="module" src="/app.js?v=20260730-table-wrap-menu-v1"></script>
|
|
880
888
|
</body>
|
|
881
889
|
</html>
|
package/dist/public/markdown.js
CHANGED
|
@@ -128,7 +128,12 @@ function tableAlignments(cells) {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
function renderMarkdownTable(headers, alignments, rows) {
|
|
131
|
-
const renderCell = (tag, value, alignment) =>
|
|
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 }
|
|
@@ -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
|
+
}
|
package/dist/public/styles.css
CHANGED
|
@@ -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-
|
|
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;
|
|
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
|
-
@
|
|
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-
|
|
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:
|
|
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),
|