@musnows/scriverse 0.5.12 → 0.6.1

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
@@ -16,6 +16,7 @@ const defaultPlatformPageSizes = {
16
16
  timeline: 30,
17
17
  outlines: 30,
18
18
  relationships: 30,
19
+ comments: 30,
19
20
  reviews: 30,
20
21
  analysisTasks: 30,
21
22
  fileVersions: 30
@@ -39,6 +40,7 @@ function platformPageSizes(value) {
39
40
  timeline: pageSize("timeline"),
40
41
  outlines: pageSize("outlines"),
41
42
  relationships: pageSize("relationships"),
43
+ comments: pageSize("comments"),
42
44
  reviews: pageSize("reviews"),
43
45
  analysisTasks: pageSize("analysisTasks"),
44
46
  fileVersions: pageSize("fileVersions")
@@ -123,6 +125,10 @@ export const versionedEntityTypes = [
123
125
  "chapter-outline",
124
126
  "foreshadow"
125
127
  ];
128
+ export function defaultAiConversationTitle(prompt) {
129
+ const normalized = prompt.replace(/\s+/gu, " ").trim();
130
+ return Array.from(normalized).slice(0, 15).join("") || "新对话";
131
+ }
126
132
  function isRecord(value) {
127
133
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
128
134
  }
@@ -689,6 +695,9 @@ export class Store {
689
695
  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"])
690
696
  .map((tool) => tool === "query_story_knowledge" ? "search_story_entities" : tool)
691
697
  .filter((tool, index, tools) => tools.indexOf(tool) === index),
698
+ titleGenerationModelId: row?.title_generation_model_id === null || row?.title_generation_model_id === undefined
699
+ ? null
700
+ : String(row.title_generation_model_id),
692
701
  updatedAt: String(row?.updated_at ?? "")
693
702
  };
694
703
  }
@@ -705,12 +714,15 @@ export class Store {
705
714
  const nextBookSummaryContextPercent = input.bookSummaryContextPercent ?? Number(current.bookSummaryContextPercent);
706
715
  const nextContextCompactThreshold = input.contextCompactThreshold ?? Number(current.contextCompactThreshold);
707
716
  const nextAgentTools = input.agentTools ?? current.agentTools;
717
+ const nextTitleGenerationModelId = input.titleGenerationModelId === undefined
718
+ ? (current.titleGenerationModelId ? String(current.titleGenerationModelId) : null)
719
+ : input.titleGenerationModelId?.trim() || null;
708
720
  this.db.run(`INSERT INTO work_ai_settings (
709
721
  work_id, system_prompt, auto_run_enabled, auto_run_concurrency, auto_run_batch_limit,
710
722
  auto_run_daily_task_limit, auto_run_failure_threshold, auto_run_paused, auto_run_pause_reason,
711
723
  auto_run_resume_at, auto_run_consecutive_failures, book_summary_context_percent,
712
- context_compact_threshold, agent_tools_json, updated_at
713
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
724
+ context_compact_threshold, agent_tools_json, title_generation_model_id, updated_at
725
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
714
726
  ON CONFLICT(work_id) DO UPDATE SET
715
727
  system_prompt = excluded.system_prompt,
716
728
  auto_run_enabled = excluded.auto_run_enabled,
@@ -725,7 +737,8 @@ export class Store {
725
737
  book_summary_context_percent = excluded.book_summary_context_percent,
726
738
  context_compact_threshold = excluded.context_compact_threshold,
727
739
  agent_tools_json = excluded.agent_tools_json,
728
- updated_at = excluded.updated_at`, workId, nextPrompt, nextEnabled ? 1 : 0, Math.min(8, Math.max(1, nextConcurrency)), Math.min(200, Math.max(1, nextBatchLimit)), Math.min(10_000, Math.max(0, nextDailyTaskLimit)), Math.min(10, Math.max(1, nextFailureThreshold)), current.autoRunPaused ? 1 : 0, String(current.autoRunPauseReason ?? ""), current.autoRunResumeAt === null ? null : String(current.autoRunResumeAt), Math.max(0, Number(current.autoRunConsecutiveFailures) || 0), Math.min(90, Math.max(1, nextBookSummaryContextPercent)), Math.min(90, Math.max(50, nextContextCompactThreshold)), JSON.stringify(nextAgentTools), timestamp);
740
+ title_generation_model_id = excluded.title_generation_model_id,
741
+ updated_at = excluded.updated_at`, workId, nextPrompt, nextEnabled ? 1 : 0, Math.min(8, Math.max(1, nextConcurrency)), Math.min(200, Math.max(1, nextBatchLimit)), Math.min(10_000, Math.max(0, nextDailyTaskLimit)), Math.min(10, Math.max(1, nextFailureThreshold)), current.autoRunPaused ? 1 : 0, String(current.autoRunPauseReason ?? ""), current.autoRunResumeAt === null ? null : String(current.autoRunResumeAt), Math.max(0, Number(current.autoRunConsecutiveFailures) || 0), Math.min(90, Math.max(1, nextBookSummaryContextPercent)), Math.min(90, Math.max(50, nextContextCompactThreshold)), JSON.stringify(nextAgentTools), nextTitleGenerationModelId, timestamp);
729
742
  this.audit(workId, "work.ai-settings.updated", "work-ai-settings", workId, {
730
743
  systemPromptChanged: input.systemPrompt !== undefined,
731
744
  autoRunEnabled: nextEnabled,
@@ -735,7 +748,8 @@ export class Store {
735
748
  autoRunFailureThreshold: Math.min(10, Math.max(1, nextFailureThreshold)),
736
749
  bookSummaryContextPercent: Math.min(90, Math.max(1, nextBookSummaryContextPercent)),
737
750
  contextCompactThreshold: Math.min(90, Math.max(50, nextContextCompactThreshold)),
738
- agentTools: nextAgentTools
751
+ agentTools: nextAgentTools,
752
+ titleGenerationModelId: nextTitleGenerationModelId
739
753
  });
740
754
  return this.getWorkAiSettings(workId);
741
755
  }
@@ -1561,6 +1575,46 @@ export class Store {
1561
1575
  WHERE annotation.chapter_id = ? AND annotation.deleted_at IS NULL
1562
1576
  ORDER BY CASE annotation.status WHEN 'open' THEN 0 ELSE 1 END, annotation.start_line, annotation.created_at`, chapterId).map((row) => this.mapChapterAnnotation(row));
1563
1577
  }
1578
+ listWorkChapterAnnotations(workId) {
1579
+ this.getWork(workId);
1580
+ return this.db.all(`SELECT annotation.*, user.display_name AS actor_display_name, user.username AS actor_username,
1581
+ chapter.title AS chapter_title, volume.title AS volume_title
1582
+ FROM chapter_annotations annotation
1583
+ JOIN chapters chapter ON chapter.id = annotation.chapter_id
1584
+ JOIN volumes volume ON volume.id = chapter.volume_id
1585
+ LEFT JOIN users user ON user.id = annotation.updated_by_user_id
1586
+ WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL
1587
+ ORDER BY CASE annotation.status WHEN 'open' THEN 0 ELSE 1 END,
1588
+ volume.sort_order, volume.created_at, chapter.sort_order, chapter.created_at,
1589
+ annotation.start_line, annotation.created_at`, workId).map((row) => ({
1590
+ ...this.mapChapterAnnotation(row),
1591
+ volumeTitle: requiredString(row, "volume_title"),
1592
+ chapterTitle: requiredString(row, "chapter_title")
1593
+ }));
1594
+ }
1595
+ listWorkChapterAnnotationsPage(workId, pagination) {
1596
+ this.getWork(workId);
1597
+ const page = paginationSql(pagination);
1598
+ const rows = this.db.all(`SELECT annotation.*, user.display_name AS actor_display_name, user.username AS actor_username,
1599
+ chapter.title AS chapter_title, volume.title AS volume_title
1600
+ FROM chapter_annotations annotation
1601
+ JOIN chapters chapter ON chapter.id = annotation.chapter_id
1602
+ JOIN volumes volume ON volume.id = chapter.volume_id
1603
+ LEFT JOIN users user ON user.id = annotation.updated_by_user_id
1604
+ WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL
1605
+ ORDER BY CASE annotation.status WHEN 'open' THEN 0 ELSE 1 END,
1606
+ volume.sort_order, volume.created_at, chapter.sort_order, chapter.created_at,
1607
+ annotation.start_line, annotation.created_at${page.sql}`, workId, ...page.params);
1608
+ const total = numberValue(this.db.get(`SELECT COUNT(*) AS count
1609
+ FROM chapter_annotations annotation
1610
+ JOIN chapters chapter ON chapter.id = annotation.chapter_id
1611
+ WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL`, workId) ?? {}, "count");
1612
+ return paginated(rows.map((row) => ({
1613
+ ...this.mapChapterAnnotation(row),
1614
+ volumeTitle: requiredString(row, "volume_title"),
1615
+ chapterTitle: requiredString(row, "chapter_title")
1616
+ })), pagination, total);
1617
+ }
1564
1618
  createChapterAnnotation(chapterId, input) {
1565
1619
  const chapter = this.getChapter(chapterId);
1566
1620
  const lines = String(chapter.content).replace(/\r\n?/gu, "\n").split("\n");
@@ -2226,7 +2280,7 @@ export class Store {
2226
2280
  this.db.run(`INSERT INTO drafts (id, work_id, draft_type, title, content, created_at, updated_at)
2227
2281
  VALUES (?, ?, ?, ?, ?, ?, ?)`, draftId, workId, input.draftType, input.title, input.content, timestamp, timestamp);
2228
2282
  this.syncMarkdownAttachmentReferences(workId, "draft", draftId, input.content);
2229
- this.recordEntityVersion("draft", draftId, source, sourceRef, changeNote || "建立创作草稿", timestamp);
2283
+ this.recordEntityVersion("draft", draftId, source, sourceRef, changeNote || "建立创作想法", timestamp);
2230
2284
  this.audit(workId, source === "restore" ? "draft.restored" : "draft.created", "draft", draftId, {
2231
2285
  draftType: input.draftType,
2232
2286
  source,
@@ -2266,17 +2320,17 @@ export class Store {
2266
2320
  getDraft(draftId) {
2267
2321
  const row = this.db.get("SELECT * FROM drafts WHERE id = ?", draftId);
2268
2322
  if (!row)
2269
- throw notFound("草稿");
2323
+ throw notFound("想法");
2270
2324
  return this.mapDraft(row, true);
2271
2325
  }
2272
2326
  updateDraft(draftId, input, source = "manual", sourceRef = null, changeNote = "", expectedVersionNo) {
2273
2327
  const current = this.getDraft(draftId);
2274
2328
  const content = input.content ?? String(current.content);
2275
2329
  this.db.transaction(() => {
2276
- this.assertExpectedVersion("draft", draftId, expectedVersionNo, "草稿");
2330
+ this.assertExpectedVersion("draft", draftId, expectedVersionNo, "想法");
2277
2331
  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
2332
  this.syncMarkdownAttachmentReferences(String(current.workId), "draft", draftId, content);
2279
- this.recordEntityVersion("draft", draftId, source, sourceRef, changeNote || "更新创作草稿");
2333
+ this.recordEntityVersion("draft", draftId, source, sourceRef, changeNote || "更新创作想法");
2280
2334
  this.audit(String(current.workId), "draft.updated", "draft", draftId, { fields: Object.keys(input), source, sourceRef });
2281
2335
  });
2282
2336
  return this.getDraft(draftId);
@@ -2284,8 +2338,8 @@ export class Store {
2284
2338
  deleteDraft(draftId, expectedVersionNo) {
2285
2339
  const current = this.getDraft(draftId);
2286
2340
  this.db.transaction(() => {
2287
- this.assertExpectedVersion("draft", draftId, expectedVersionNo, "草稿");
2288
- this.recordEntityVersion("draft", draftId, "delete", null, "删除创作草稿");
2341
+ this.assertExpectedVersion("draft", draftId, expectedVersionNo, "想法");
2342
+ this.recordEntityVersion("draft", draftId, "delete", null, "删除创作想法");
2289
2343
  this.clearMarkdownAttachmentReferences("draft", draftId);
2290
2344
  this.db.run("DELETE FROM drafts WHERE id = ?", draftId);
2291
2345
  this.audit(String(current.workId), "draft.deleted", "draft", draftId);
@@ -4098,6 +4152,16 @@ export class Store {
4098
4152
  ORDER BY conversation.updated_at DESC, conversation.created_at DESC${page.sql}`, workId, ...page.params);
4099
4153
  return paginated(rows.map((row) => this.mapAiConversation(row)), pagination);
4100
4154
  }
4155
+ getAiConversationSummary(conversationId) {
4156
+ const row = this.db.get(`SELECT conversation.*,
4157
+ (SELECT COUNT(*) FROM ai_conversation_messages message WHERE message.conversation_id = conversation.id) AS message_count,
4158
+ COALESCE((SELECT content FROM ai_conversation_messages message WHERE message.conversation_id = conversation.id ORDER BY message.created_at DESC, message.rowid DESC LIMIT 1), '') AS preview
4159
+ FROM ai_conversations conversation
4160
+ WHERE conversation.id = ?`, conversationId);
4161
+ if (!row)
4162
+ throw notFound("AI 对话");
4163
+ return this.mapAiConversation(row);
4164
+ }
4101
4165
  getAiConversation(conversationId) {
4102
4166
  const row = this.db.get("SELECT * FROM ai_conversations WHERE id = ?", conversationId);
4103
4167
  if (!row)
@@ -4127,7 +4191,7 @@ export class Store {
4127
4191
  throw notFound("AI 对话");
4128
4192
  if (requiredString(conversation, "work_id") !== workId)
4129
4193
  throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
4130
- const rows = this.db.all("SELECT id, role, content FROM ai_conversation_messages WHERE conversation_id = ? ORDER BY created_at, rowid", conversationId);
4194
+ const rows = this.db.all("SELECT id, role, content, metadata_json FROM ai_conversation_messages WHERE conversation_id = ? ORDER BY created_at, rowid", conversationId);
4131
4195
  const compactedMessageCount = Math.min(rows.length, Math.max(0, numberValue(conversation, "compacted_message_count")));
4132
4196
  return {
4133
4197
  workId,
@@ -4140,10 +4204,23 @@ export class Store {
4140
4204
  .map((message) => ({
4141
4205
  id: requiredString(message, "id"),
4142
4206
  role: requiredString(message, "role") === "assistant" ? "assistant" : "user",
4143
- content: requiredString(message, "content")
4207
+ content: requiredString(message, "content"),
4208
+ metadata: json(requiredString(message, "metadata_json"), {})
4144
4209
  }))
4145
4210
  };
4146
4211
  }
4212
+ getAiConversationTitleContext(conversationId, workId) {
4213
+ const conversation = this.db.get("SELECT title, work_id FROM ai_conversations WHERE id = ?", conversationId);
4214
+ if (!conversation)
4215
+ throw notFound("AI 对话");
4216
+ if (requiredString(conversation, "work_id") !== workId)
4217
+ throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
4218
+ const messages = this.db.all("SELECT role, content FROM ai_conversation_messages WHERE conversation_id = ? ORDER BY created_at, rowid", conversationId).map((message) => ({
4219
+ role: requiredString(message, "role") === "assistant" ? "assistant" : "user",
4220
+ content: requiredString(message, "content")
4221
+ }));
4222
+ return { title: requiredString(conversation, "title"), messages };
4223
+ }
4147
4224
  setAiConversationContextWarning(conversationId, pending) {
4148
4225
  const conversation = this.db.get("SELECT id FROM ai_conversations WHERE id = ?", conversationId);
4149
4226
  if (!conversation)
@@ -4157,6 +4234,14 @@ export class Store {
4157
4234
  this.db.run("UPDATE ai_conversations SET compacted_summary = ?, compacted_message_count = ?, context_warning_at = NULL, updated_at = ? WHERE id = ?", summary, Math.max(0, compactedMessageCount), now(), conversationId);
4158
4235
  return this.getAiConversation(conversationId);
4159
4236
  }
4237
+ setAiConversationTitle(conversationId, title) {
4238
+ const conversation = this.db.get("SELECT id FROM ai_conversations WHERE id = ?", conversationId);
4239
+ if (!conversation)
4240
+ throw notFound("AI 对话");
4241
+ const normalizedTitle = title.replace(/\s+/gu, " ").trim().slice(0, 200) || "新对话";
4242
+ this.db.run("UPDATE ai_conversations SET title = ?, updated_at = ? WHERE id = ?", normalizedTitle, now(), conversationId);
4243
+ return this.getAiConversation(conversationId);
4244
+ }
4160
4245
  addAiConversationMessage(conversationId, input) {
4161
4246
  const conversation = this.db.get("SELECT * FROM ai_conversations WHERE id = ?", conversationId);
4162
4247
  if (!conversation)
@@ -4170,7 +4255,7 @@ export class Store {
4170
4255
  const messageId = id("message");
4171
4256
  const timestamp = now();
4172
4257
  const title = requiredString(conversation, "title") === "新对话" && input.role === "user"
4173
- ? input.content.replace(/\s+/gu, " ").trim().slice(0, 36) || "新对话"
4258
+ ? defaultAiConversationTitle(input.content)
4174
4259
  : requiredString(conversation, "title");
4175
4260
  this.db.transaction(() => {
4176
4261
  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);