@musnows/scriverse 0.5.10 → 0.5.12
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 +80 -5
- package/dist/ai.js.map +1 -1
- package/dist/app.js +61 -4
- 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 +66 -1
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +666 -110
- 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/module-request-cache.d.ts +17 -0
- package/dist/public/module-request-cache.js +35 -0
- 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 +83 -0
- package/dist/public/styles.css +70 -6
- package/dist/public/work-permissions.d.ts +2 -2
- package/dist/public/work-permissions.js +8 -0
- package/dist/store.js +164 -18
- 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/dist/writing-progress-time.js +71 -0
- package/dist/writing-progress-time.js.map +1 -0
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -6,7 +6,9 @@ import { paginated, paginationSql } from "./pagination.js";
|
|
|
6
6
|
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
|
+
import { buildWritingCalendar, writingDateKey } from "./writing-progress-time.js";
|
|
9
10
|
const defaultPlatformPageSizes = {
|
|
11
|
+
drafts: 30,
|
|
10
12
|
settings: 30,
|
|
11
13
|
characters: 30,
|
|
12
14
|
races: 30,
|
|
@@ -29,6 +31,7 @@ function platformPageSizes(value) {
|
|
|
29
31
|
: defaultPlatformPageSizes[key];
|
|
30
32
|
};
|
|
31
33
|
return {
|
|
34
|
+
drafts: pageSize("drafts"),
|
|
32
35
|
settings: pageSize("settings"),
|
|
33
36
|
characters: pageSize("characters"),
|
|
34
37
|
races: pageSize("races"),
|
|
@@ -110,6 +113,7 @@ function settingsFromKnowledgeSections(sections) {
|
|
|
110
113
|
export const versionedEntityTypes = [
|
|
111
114
|
"work",
|
|
112
115
|
"volume",
|
|
116
|
+
"draft",
|
|
113
117
|
"setting",
|
|
114
118
|
"race",
|
|
115
119
|
"organization",
|
|
@@ -237,6 +241,8 @@ export class Store {
|
|
|
237
241
|
return this.getWork(entityId);
|
|
238
242
|
if (type === "volume")
|
|
239
243
|
return this.getVolume(entityId);
|
|
244
|
+
if (type === "draft")
|
|
245
|
+
return this.getDraft(entityId);
|
|
240
246
|
if (type === "setting")
|
|
241
247
|
return this.getSetting(entityId);
|
|
242
248
|
if (type === "race")
|
|
@@ -287,6 +293,12 @@ export class Store {
|
|
|
287
293
|
keywords: entity.keywords,
|
|
288
294
|
sortOrder: entity.sortOrder
|
|
289
295
|
};
|
|
296
|
+
if (type === "draft")
|
|
297
|
+
return {
|
|
298
|
+
draftType: entity.draftType,
|
|
299
|
+
title: entity.title,
|
|
300
|
+
content: entity.content
|
|
301
|
+
};
|
|
290
302
|
if (type === "setting")
|
|
291
303
|
return {
|
|
292
304
|
title: entity.title,
|
|
@@ -393,6 +405,7 @@ export class Store {
|
|
|
393
405
|
const entities = [
|
|
394
406
|
...this.db.all("SELECT id, updated_at FROM works").map((row) => ["work", requiredString(row, "id"), requiredString(row, "updated_at")]),
|
|
395
407
|
...this.db.all("SELECT id, updated_at FROM volumes").map((row) => ["volume", requiredString(row, "id"), requiredString(row, "updated_at")]),
|
|
408
|
+
...this.db.all("SELECT id, updated_at FROM drafts").map((row) => ["draft", requiredString(row, "id"), requiredString(row, "updated_at")]),
|
|
396
409
|
...this.db.all("SELECT id, updated_at FROM settings").map((row) => ["setting", requiredString(row, "id"), requiredString(row, "updated_at")]),
|
|
397
410
|
...this.db.all("SELECT id, updated_at FROM races").map((row) => ["race", requiredString(row, "id"), requiredString(row, "updated_at")]),
|
|
398
411
|
...this.db.all("SELECT id, updated_at FROM organizations").map((row) => ["organization", requiredString(row, "id"), requiredString(row, "updated_at")]),
|
|
@@ -474,6 +487,8 @@ export class Store {
|
|
|
474
487
|
restored = this.updateWork(entityId, snapshot, expectedVersionNo, "restore", sourceRef, changeNote);
|
|
475
488
|
else if (type === "volume")
|
|
476
489
|
restored = this.updateVolume(entityId, snapshot, expectedVersionNo, "restore", sourceRef, changeNote);
|
|
490
|
+
else if (type === "draft")
|
|
491
|
+
restored = this.updateDraft(entityId, snapshot, "restore", sourceRef, changeNote, expectedVersionNo);
|
|
477
492
|
else if (type === "setting")
|
|
478
493
|
restored = this.updateSetting(entityId, snapshot, "restore", sourceRef, changeNote, expectedVersionNo);
|
|
479
494
|
else if (type === "race")
|
|
@@ -513,6 +528,9 @@ export class Store {
|
|
|
513
528
|
if (type === "volume") {
|
|
514
529
|
return this.db.transaction(() => this.insertVolumeWithId(workId, entityId, snapshot, "restore", sourceRef, changeNote));
|
|
515
530
|
}
|
|
531
|
+
if (type === "draft") {
|
|
532
|
+
return this.insertDraftWithId(workId, entityId, snapshot, "restore", sourceRef, changeNote);
|
|
533
|
+
}
|
|
516
534
|
if (type === "setting") {
|
|
517
535
|
return this.insertSettingWithId(workId, entityId, snapshot, "restore", sourceRef, changeNote);
|
|
518
536
|
}
|
|
@@ -668,7 +686,7 @@ export class Store {
|
|
|
668
686
|
autoRunConsecutiveFailures: Math.max(0, Number(row?.auto_run_consecutive_failures ?? 0) || 0),
|
|
669
687
|
bookSummaryContextPercent: Math.min(90, Math.max(1, Number(row?.book_summary_context_percent ?? 50) || 50)),
|
|
670
688
|
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"])
|
|
689
|
+
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
690
|
.map((tool) => tool === "query_story_knowledge" ? "search_story_entities" : tool)
|
|
673
691
|
.filter((tool, index, tools) => tools.indexOf(tool) === index),
|
|
674
692
|
updatedAt: String(row?.updated_at ?? "")
|
|
@@ -874,6 +892,41 @@ export class Store {
|
|
|
874
892
|
}));
|
|
875
893
|
return { ...work, volumes };
|
|
876
894
|
}
|
|
895
|
+
getWorkVolumeDirectory(workId) {
|
|
896
|
+
const work = this.getWork(workId);
|
|
897
|
+
const permissions = work.modulePermissions;
|
|
898
|
+
if (permissions.prose === "none")
|
|
899
|
+
return { ...work, volumes: [] };
|
|
900
|
+
const volumeRows = this.db.all(`SELECT volume.*,
|
|
901
|
+
(SELECT COUNT(*) FROM chapters chapter WHERE chapter.volume_id = volume.id AND chapter.deleted_at IS NULL) AS chapter_count
|
|
902
|
+
FROM volumes volume WHERE volume.work_id = ? ORDER BY volume.sort_order, volume.created_at`, workId);
|
|
903
|
+
const volumes = volumeRows.map((row) => ({
|
|
904
|
+
...this.mapVolume(row),
|
|
905
|
+
chapterCount: numberValue(row, "chapter_count"),
|
|
906
|
+
chapters: []
|
|
907
|
+
}));
|
|
908
|
+
return { ...work, volumes };
|
|
909
|
+
}
|
|
910
|
+
listVolumeChapters(volumeId) {
|
|
911
|
+
const volume = this.getVolume(volumeId);
|
|
912
|
+
const work = this.getWork(String(volume.workId));
|
|
913
|
+
if (work.modulePermissions.prose === "none")
|
|
914
|
+
return [];
|
|
915
|
+
return this.db.all(`SELECT id, work_id, volume_id, title, chapter_type, sort_order, word_count, version_no,
|
|
916
|
+
analysis_status, excluded_from_analysis, created_at, updated_at
|
|
917
|
+
FROM chapters WHERE volume_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at`, volumeId).map((row) => this.mapChapterDirectoryEntry(row));
|
|
918
|
+
}
|
|
919
|
+
listVolumeChaptersPage(volumeId, pagination) {
|
|
920
|
+
const volume = this.getVolume(volumeId);
|
|
921
|
+
const work = this.getWork(String(volume.workId));
|
|
922
|
+
if (work.modulePermissions.prose === "none")
|
|
923
|
+
return paginated([], pagination);
|
|
924
|
+
const page = paginationSql(pagination);
|
|
925
|
+
const rows = this.db.all(`SELECT id, work_id, volume_id, title, chapter_type, sort_order, word_count, version_no,
|
|
926
|
+
analysis_status, excluded_from_analysis, created_at, updated_at
|
|
927
|
+
FROM chapters WHERE volume_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at${page.sql}`, volumeId, ...page.params);
|
|
928
|
+
return paginated(rows.map((row) => this.mapChapterDirectoryEntry(row)), pagination);
|
|
929
|
+
}
|
|
877
930
|
getWorkDirectoryPage(workId, pagination) {
|
|
878
931
|
const work = this.getWork(workId);
|
|
879
932
|
const permissions = work.modulePermissions;
|
|
@@ -2163,6 +2216,94 @@ export class Store {
|
|
|
2163
2216
|
FROM chapters c JOIN volumes v ON v.id = c.volume_id WHERE c.id = ? AND c.work_id = ?`, chapterId, workId);
|
|
2164
2217
|
return row ? numberValue(row, "sequence") : Number.MAX_SAFE_INTEGER;
|
|
2165
2218
|
}
|
|
2219
|
+
createDraft(workId, input, source = "create", sourceRef = null) {
|
|
2220
|
+
this.getWork(workId);
|
|
2221
|
+
return this.insertDraftWithId(workId, id("draft"), input, source, sourceRef);
|
|
2222
|
+
}
|
|
2223
|
+
insertDraftWithId(workId, draftId, input, source = "create", sourceRef = null, changeNote = "") {
|
|
2224
|
+
const timestamp = now();
|
|
2225
|
+
this.db.transaction(() => {
|
|
2226
|
+
this.db.run(`INSERT INTO drafts (id, work_id, draft_type, title, content, created_at, updated_at)
|
|
2227
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, draftId, workId, input.draftType, input.title, input.content, timestamp, timestamp);
|
|
2228
|
+
this.syncMarkdownAttachmentReferences(workId, "draft", draftId, input.content);
|
|
2229
|
+
this.recordEntityVersion("draft", draftId, source, sourceRef, changeNote || "建立创作草稿", timestamp);
|
|
2230
|
+
this.audit(workId, source === "restore" ? "draft.restored" : "draft.created", "draft", draftId, {
|
|
2231
|
+
draftType: input.draftType,
|
|
2232
|
+
source,
|
|
2233
|
+
sourceRef
|
|
2234
|
+
});
|
|
2235
|
+
});
|
|
2236
|
+
return this.getDraft(draftId);
|
|
2237
|
+
}
|
|
2238
|
+
listDrafts(workId, draftType, includeContent = false) {
|
|
2239
|
+
this.getWork(workId);
|
|
2240
|
+
return this.db.all(`SELECT * FROM drafts WHERE work_id = ? AND (? IS NULL OR draft_type = ?)
|
|
2241
|
+
ORDER BY updated_at DESC, title`, workId, draftType ?? null, draftType ?? null).map((row) => this.mapDraft(row, includeContent));
|
|
2242
|
+
}
|
|
2243
|
+
listDraftsPage(workId, pagination, draftType, includeContent = false) {
|
|
2244
|
+
this.getWork(workId);
|
|
2245
|
+
const page = paginationSql(pagination);
|
|
2246
|
+
const rows = this.db.all(`SELECT * FROM drafts WHERE work_id = ? AND (? IS NULL OR draft_type = ?)
|
|
2247
|
+
ORDER BY updated_at DESC, title${page.sql}`, workId, draftType ?? null, draftType ?? null, ...page.params);
|
|
2248
|
+
return paginated(rows.map((row) => this.mapDraft(row, includeContent)), pagination);
|
|
2249
|
+
}
|
|
2250
|
+
searchDrafts(workId, query, draftType, limit = 20) {
|
|
2251
|
+
this.getWork(workId);
|
|
2252
|
+
const safeLimit = Math.min(30, Math.max(1, Math.trunc(limit)));
|
|
2253
|
+
const normalizedQuery = query.normalize("NFKC").trim();
|
|
2254
|
+
const escapedQuery = normalizedQuery.replace(/[\\%_]/gu, "\\$&");
|
|
2255
|
+
const pattern = `%${escapedQuery}%`;
|
|
2256
|
+
const rows = normalizedQuery
|
|
2257
|
+
? this.db.all(`SELECT * FROM drafts
|
|
2258
|
+
WHERE work_id = ? AND (? IS NULL OR draft_type = ?)
|
|
2259
|
+
AND (title LIKE ? ESCAPE '\\' COLLATE NOCASE OR content LIKE ? ESCAPE '\\' COLLATE NOCASE)
|
|
2260
|
+
ORDER BY CASE WHEN title LIKE ? ESCAPE '\\' COLLATE NOCASE THEN 0 ELSE 1 END, updated_at DESC
|
|
2261
|
+
LIMIT ?`, workId, draftType ?? null, draftType ?? null, pattern, pattern, pattern, safeLimit)
|
|
2262
|
+
: this.db.all(`SELECT * FROM drafts WHERE work_id = ? AND (? IS NULL OR draft_type = ?)
|
|
2263
|
+
ORDER BY updated_at DESC, title LIMIT ?`, workId, draftType ?? null, draftType ?? null, safeLimit);
|
|
2264
|
+
return rows.map((row) => this.mapDraft(row, true));
|
|
2265
|
+
}
|
|
2266
|
+
getDraft(draftId) {
|
|
2267
|
+
const row = this.db.get("SELECT * FROM drafts WHERE id = ?", draftId);
|
|
2268
|
+
if (!row)
|
|
2269
|
+
throw notFound("草稿");
|
|
2270
|
+
return this.mapDraft(row, true);
|
|
2271
|
+
}
|
|
2272
|
+
updateDraft(draftId, input, source = "manual", sourceRef = null, changeNote = "", expectedVersionNo) {
|
|
2273
|
+
const current = this.getDraft(draftId);
|
|
2274
|
+
const content = input.content ?? String(current.content);
|
|
2275
|
+
this.db.transaction(() => {
|
|
2276
|
+
this.assertExpectedVersion("draft", draftId, expectedVersionNo, "草稿");
|
|
2277
|
+
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);
|
|
2278
|
+
this.syncMarkdownAttachmentReferences(String(current.workId), "draft", draftId, content);
|
|
2279
|
+
this.recordEntityVersion("draft", draftId, source, sourceRef, changeNote || "更新创作草稿");
|
|
2280
|
+
this.audit(String(current.workId), "draft.updated", "draft", draftId, { fields: Object.keys(input), source, sourceRef });
|
|
2281
|
+
});
|
|
2282
|
+
return this.getDraft(draftId);
|
|
2283
|
+
}
|
|
2284
|
+
deleteDraft(draftId, expectedVersionNo) {
|
|
2285
|
+
const current = this.getDraft(draftId);
|
|
2286
|
+
this.db.transaction(() => {
|
|
2287
|
+
this.assertExpectedVersion("draft", draftId, expectedVersionNo, "草稿");
|
|
2288
|
+
this.recordEntityVersion("draft", draftId, "delete", null, "删除创作草稿");
|
|
2289
|
+
this.clearMarkdownAttachmentReferences("draft", draftId);
|
|
2290
|
+
this.db.run("DELETE FROM drafts WHERE id = ?", draftId);
|
|
2291
|
+
this.audit(String(current.workId), "draft.deleted", "draft", draftId);
|
|
2292
|
+
});
|
|
2293
|
+
}
|
|
2294
|
+
mapDraft(row, includeContent) {
|
|
2295
|
+
const content = requiredString(row, "content");
|
|
2296
|
+
return {
|
|
2297
|
+
id: requiredString(row, "id"),
|
|
2298
|
+
workId: requiredString(row, "work_id"),
|
|
2299
|
+
draftType: requiredString(row, "draft_type"),
|
|
2300
|
+
title: requiredString(row, "title"),
|
|
2301
|
+
...(includeContent ? { content } : { contentPreview: content.replace(/\s+/gu, " ").trim().slice(0, 320) }),
|
|
2302
|
+
versionNo: this.currentEntityVersionNo("draft", requiredString(row, "id")),
|
|
2303
|
+
createdAt: requiredString(row, "created_at"),
|
|
2304
|
+
updatedAt: requiredString(row, "updated_at")
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2166
2307
|
createSetting(workId, input, source = "create", sourceRef = null) {
|
|
2167
2308
|
this.getWork(workId);
|
|
2168
2309
|
return this.insertSettingWithId(workId, id("setting"), input, source, sourceRef);
|
|
@@ -4020,16 +4161,26 @@ export class Store {
|
|
|
4020
4161
|
const conversation = this.db.get("SELECT * FROM ai_conversations WHERE id = ?", conversationId);
|
|
4021
4162
|
if (!conversation)
|
|
4022
4163
|
throw notFound("AI 对话");
|
|
4164
|
+
const requestId = input.requestId?.trim() || null;
|
|
4165
|
+
if (requestId) {
|
|
4166
|
+
const existing = this.db.get("SELECT * FROM ai_conversation_messages WHERE conversation_id = ? AND request_id = ?", conversationId, requestId);
|
|
4167
|
+
if (existing)
|
|
4168
|
+
return this.mapAiConversationMessage(existing);
|
|
4169
|
+
}
|
|
4023
4170
|
const messageId = id("message");
|
|
4024
4171
|
const timestamp = now();
|
|
4025
4172
|
const title = requiredString(conversation, "title") === "新对话" && input.role === "user"
|
|
4026
4173
|
? input.content.replace(/\s+/gu, " ").trim().slice(0, 36) || "新对话"
|
|
4027
4174
|
: requiredString(conversation, "title");
|
|
4028
4175
|
this.db.transaction(() => {
|
|
4029
|
-
this.db.run("INSERT INTO ai_conversation_messages (id, conversation_id, role, content, citations_json, metadata_json, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", messageId, conversationId, input.role, input.content, JSON.stringify(input.citations ?? []), JSON.stringify(input.metadata ?? {}), timestamp, currentRequestActor()?.userId ?? null);
|
|
4030
|
-
this.db.
|
|
4176
|
+
this.db.run("INSERT INTO ai_conversation_messages (id, conversation_id, role, content, citations_json, metadata_json, request_id, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(conversation_id, request_id) WHERE request_id IS NOT NULL DO NOTHING", messageId, conversationId, input.role, input.content, JSON.stringify(input.citations ?? []), JSON.stringify(input.metadata ?? {}), requestId, timestamp, currentRequestActor()?.userId ?? null);
|
|
4177
|
+
const inserted = this.db.get("SELECT id FROM ai_conversation_messages WHERE id = ?", messageId);
|
|
4178
|
+
if (inserted)
|
|
4179
|
+
this.db.run("UPDATE ai_conversations SET title = ?, updated_at = ? WHERE id = ?", title, timestamp, conversationId);
|
|
4031
4180
|
});
|
|
4032
|
-
const message =
|
|
4181
|
+
const message = requestId
|
|
4182
|
+
? this.db.get("SELECT * FROM ai_conversation_messages WHERE conversation_id = ? AND request_id = ?", conversationId, requestId)
|
|
4183
|
+
: this.db.get("SELECT * FROM ai_conversation_messages WHERE id = ?", messageId);
|
|
4033
4184
|
if (!message)
|
|
4034
4185
|
throw notFound("AI 对话消息");
|
|
4035
4186
|
return this.mapAiConversationMessage(message);
|
|
@@ -4052,7 +4203,7 @@ export class Store {
|
|
|
4052
4203
|
this.db.transaction(() => {
|
|
4053
4204
|
this.db.run("INSERT INTO ai_conversations (id, work_id, title, compacted_summary, compacted_message_count, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", forkId, requiredString(conversation, "work_id"), title.slice(0, 200), forkSummary, forkCompactedCount, timestamp, timestamp, currentRequestActor()?.userId ?? null);
|
|
4054
4205
|
for (const message of messages.slice(0, targetIndex + 1)) {
|
|
4055
|
-
this.db.run("INSERT INTO ai_conversation_messages (id, conversation_id, role, content, citations_json, metadata_json, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", id("message"), forkId, requiredString(message, "role"), requiredString(message, "content"), requiredString(message, "citations_json"), requiredString(message, "metadata_json"), requiredString(message, "created_at"), currentRequestActor()?.userId ?? null);
|
|
4206
|
+
this.db.run("INSERT INTO ai_conversation_messages (id, conversation_id, role, content, citations_json, metadata_json, request_id, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", id("message"), forkId, requiredString(message, "role"), requiredString(message, "content"), requiredString(message, "citations_json"), requiredString(message, "metadata_json"), optionalString(message, "request_id"), requiredString(message, "created_at"), currentRequestActor()?.userId ?? null);
|
|
4056
4207
|
}
|
|
4057
4208
|
});
|
|
4058
4209
|
return this.getAiConversation(forkId);
|
|
@@ -4079,6 +4230,7 @@ export class Store {
|
|
|
4079
4230
|
content: requiredString(row, "content"),
|
|
4080
4231
|
citations: json(requiredString(row, "citations_json"), []),
|
|
4081
4232
|
metadata: json(requiredString(row, "metadata_json"), {}),
|
|
4233
|
+
requestId: optionalString(row, "request_id"),
|
|
4082
4234
|
createdAt: requiredString(row, "created_at")
|
|
4083
4235
|
};
|
|
4084
4236
|
}
|
|
@@ -5445,9 +5597,10 @@ export class Store {
|
|
|
5445
5597
|
exportWork(workId) {
|
|
5446
5598
|
const tree = this.getWorkTree(workId);
|
|
5447
5599
|
return {
|
|
5448
|
-
schemaVersion:
|
|
5600
|
+
schemaVersion: 8,
|
|
5449
5601
|
exportedAt: now(),
|
|
5450
5602
|
work: tree,
|
|
5603
|
+
drafts: this.listDrafts(workId, undefined, true),
|
|
5451
5604
|
settings: this.listSettings(workId),
|
|
5452
5605
|
characters: this.listCharacters(workId, true, true),
|
|
5453
5606
|
races: this.listRaces(workId),
|
|
@@ -5509,18 +5662,14 @@ export class Store {
|
|
|
5509
5662
|
const goal = this.db.get("SELECT * FROM writing_goals WHERE work_id = ?", workId);
|
|
5510
5663
|
const dailyGoal = goal ? numberValue(goal, "daily_goal") : 1000;
|
|
5511
5664
|
const targetTotal = goal ? numberValue(goal, "target_total") : 100000;
|
|
5512
|
-
const
|
|
5513
|
-
today.setUTCHours(0, 0, 0, 0);
|
|
5514
|
-
const start = new Date(today);
|
|
5515
|
-
start.setUTCDate(start.getUTCDate() - days + 1);
|
|
5516
|
-
const startKey = start.toISOString().slice(0, 10);
|
|
5665
|
+
const calendar = buildWritingCalendar(new Date(), days);
|
|
5517
5666
|
const versions = this.db.all(`SELECT chapter_id, content, source, created_at FROM chapter_versions
|
|
5518
|
-
WHERE work_id = ? AND created_at
|
|
5667
|
+
WHERE work_id = ? AND created_at < ? ORDER BY created_at, version_no, id`, workId, calendar.endExclusive);
|
|
5519
5668
|
const chapterWords = new Map();
|
|
5520
5669
|
const events = new Map();
|
|
5521
5670
|
for (const version of versions) {
|
|
5522
|
-
const day = requiredString(version, "created_at")
|
|
5523
|
-
if (day < startKey) {
|
|
5671
|
+
const day = writingDateKey(new Date(requiredString(version, "created_at")), calendar.timeZone);
|
|
5672
|
+
if (day < calendar.startKey) {
|
|
5524
5673
|
chapterWords.set(requiredString(version, "chapter_id"), requiredString(version, "source") === "delete" ? 0 : countWords(requiredString(version, "content")));
|
|
5525
5674
|
}
|
|
5526
5675
|
else {
|
|
@@ -5531,10 +5680,7 @@ export class Store {
|
|
|
5531
5680
|
}
|
|
5532
5681
|
let previousTotal = [...chapterWords.values()].reduce((sum, value) => sum + value, 0);
|
|
5533
5682
|
const trend = [];
|
|
5534
|
-
for (
|
|
5535
|
-
const date = new Date(start);
|
|
5536
|
-
date.setUTCDate(start.getUTCDate() + index);
|
|
5537
|
-
const day = date.toISOString().slice(0, 10);
|
|
5683
|
+
for (const day of calendar.dateKeys) {
|
|
5538
5684
|
for (const version of events.get(day) ?? []) {
|
|
5539
5685
|
chapterWords.set(requiredString(version, "chapter_id"), requiredString(version, "source") === "delete" ? 0 : countWords(requiredString(version, "content")));
|
|
5540
5686
|
}
|