@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/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.run("UPDATE ai_conversations SET title = ?, updated_at = ? WHERE id = ?", title, timestamp, conversationId);
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 = this.db.get("SELECT * FROM ai_conversation_messages WHERE id = ?", messageId);
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: 7,
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 today = new Date();
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 <= ? ORDER BY created_at, version_no, id`, workId, `${today.toISOString().slice(0, 10)}T23:59:59.999Z`);
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").slice(0, 10);
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 (let index = 0; index < days; index += 1) {
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
  }