@musnows/scriverse 0.8.6 → 0.8.8

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
@@ -13,6 +13,7 @@ import { countWords, documentShortSearchTerms, escapeSqlLikePattern, id, json, n
13
13
  import { buildWritingCalendar, writingDateKey } from "./writing-progress-time.js";
14
14
  import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
15
15
  import { DEFAULT_AI_STREAM_IDLE_TIMEOUT_SECONDS, normalizeAiStreamIdleTimeoutSeconds } from "./ai-stream-timeout.js";
16
+ import { normalizeRoleplayScenePin, roleplayUserTurnDisplayText, roleplayUserTurnTitleSource } from "./roleplay-turn.js";
16
17
  const WORK_LIST_BATCH_SIZE = 500;
17
18
  const ENTITY_LIST_BATCH_SIZE = 400;
18
19
  export const RECYCLE_BIN_RETENTION_DAYS = 30;
@@ -211,7 +212,7 @@ export const versionedEntityTypes = [
211
212
  export const AI_CONVERSATION_STREAM_REQUEST_LEASE_MS = 3 * 60_000;
212
213
  export const aiConversationTaskTypes = ["chat", "roleplay", "continue", "polish"];
213
214
  export function defaultAiConversationTitle(prompt) {
214
- const normalized = prompt.replace(/\s+/gu, " ").trim();
215
+ const normalized = roleplayUserTurnTitleSource(prompt).replace(/\s+/gu, " ").trim();
215
216
  return Array.from(normalized).slice(0, 15).join("") || "新对话";
216
217
  }
217
218
  function isRecord(value) {
@@ -3706,7 +3707,7 @@ export class Store {
3706
3707
  return this.db.all(`SELECT draft.*, volume.title AS volume_title FROM drafts draft
3707
3708
  LEFT JOIN volumes volume ON volume.id = draft.volume_id
3708
3709
  WHERE draft.work_id = ? AND (? IS NULL OR draft.draft_type = ?)
3709
- ORDER BY draft.updated_at DESC, draft.title`, workId, draftType ?? null, draftType ?? null).map((row) => this.mapDraft(row, includeContent));
3710
+ ORDER BY draft.is_favorite DESC, draft.updated_at DESC, draft.title`, workId, draftType ?? null, draftType ?? null).map((row) => this.mapDraft(row, includeContent));
3710
3711
  }
3711
3712
  listDraftsPage(workId, pagination, draftType, includeContent = false) {
3712
3713
  this.getWork(workId);
@@ -3714,7 +3715,7 @@ export class Store {
3714
3715
  const rows = this.db.all(`SELECT draft.*, volume.title AS volume_title FROM drafts draft
3715
3716
  LEFT JOIN volumes volume ON volume.id = draft.volume_id
3716
3717
  WHERE draft.work_id = ? AND (? IS NULL OR draft.draft_type = ?)
3717
- ORDER BY draft.updated_at DESC, draft.title${page.sql}`, workId, draftType ?? null, draftType ?? null, ...page.params);
3718
+ ORDER BY draft.is_favorite DESC, draft.updated_at DESC, draft.title${page.sql}`, workId, draftType ?? null, draftType ?? null, ...page.params);
3718
3719
  return paginated(rows.map((row) => this.mapDraft(row, includeContent)), pagination);
3719
3720
  }
3720
3721
  searchDrafts(workId, query, draftType, limit = 20) {
@@ -3743,6 +3744,21 @@ export class Store {
3743
3744
  throw notFound("想法");
3744
3745
  return this.mapDraft(row, true);
3745
3746
  }
3747
+ setDraftFavorite(draftId, isFavorite) {
3748
+ const draft = this.db.get("SELECT id, work_id, is_favorite FROM drafts WHERE id = ?", draftId);
3749
+ if (!draft)
3750
+ throw notFound("想法");
3751
+ const workId = requiredString(draft, "work_id");
3752
+ const previousFavorite = booleanValue(draft, "is_favorite");
3753
+ this.db.transaction(() => {
3754
+ this.db.run("UPDATE drafts SET is_favorite = ? WHERE id = ?", isFavorite ? 1 : 0, draftId);
3755
+ this.audit(workId, "draft.favorite-updated", "draft", draftId, {
3756
+ previousFavorite,
3757
+ isFavorite
3758
+ });
3759
+ });
3760
+ return this.getDraft(draftId);
3761
+ }
3746
3762
  updateDraft(draftId, input, source = "manual", sourceRef = null, changeNote = "", expectedVersionNo) {
3747
3763
  const current = this.getDraft(draftId);
3748
3764
  const content = input.content ?? String(current.content);
@@ -3781,6 +3797,7 @@ export class Store {
3781
3797
  volumeTitle: optionalString(row, "volume_title"),
3782
3798
  settingModule: optionalString(row, "setting_module"),
3783
3799
  title: requiredString(row, "title"),
3800
+ isFavorite: booleanValue(row, "is_favorite"),
3784
3801
  ...(includeContent ? { content } : { contentPreview: content.replace(/\s+/gu, " ").trim().slice(0, 320) }),
3785
3802
  versionNo: this.currentEntityVersionNo("draft", requiredString(row, "id")),
3786
3803
  createdAt: requiredString(row, "created_at"),
@@ -3844,12 +3861,12 @@ export class Store {
3844
3861
  }
3845
3862
  listSettings(workId, includeContent = true) {
3846
3863
  this.getWork(workId);
3847
- return this.db.all("SELECT * FROM settings WHERE work_id = ? ORDER BY locked DESC, category, title", workId).map((row) => this.mapSetting(row, includeContent));
3864
+ return this.db.all("SELECT * FROM settings WHERE work_id = ? ORDER BY is_favorite DESC, locked DESC, category, title", workId).map((row) => this.mapSetting(row, includeContent));
3848
3865
  }
3849
3866
  listSettingsPage(workId, pagination, includeContent = true) {
3850
3867
  this.getWork(workId);
3851
3868
  const page = paginationSql(pagination);
3852
- const rows = this.db.all(`SELECT * FROM settings WHERE work_id = ? ORDER BY locked DESC, category, title${page.sql}`, workId, ...page.params);
3869
+ const rows = this.db.all(`SELECT * FROM settings WHERE work_id = ? ORDER BY is_favorite DESC, locked DESC, category, title${page.sql}`, workId, ...page.params);
3853
3870
  return paginated(rows.map((row) => this.mapSetting(row, includeContent)), pagination);
3854
3871
  }
3855
3872
  getSetting(settingId) {
@@ -3858,6 +3875,21 @@ export class Store {
3858
3875
  throw notFound("设定");
3859
3876
  return this.mapSetting(row);
3860
3877
  }
3878
+ setSettingFavorite(settingId, isFavorite) {
3879
+ const setting = this.db.get("SELECT id, work_id, is_favorite FROM settings WHERE id = ?", settingId);
3880
+ if (!setting)
3881
+ throw notFound("设定");
3882
+ const workId = requiredString(setting, "work_id");
3883
+ const previousFavorite = booleanValue(setting, "is_favorite");
3884
+ this.db.transaction(() => {
3885
+ this.db.run("UPDATE settings SET is_favorite = ? WHERE id = ?", isFavorite ? 1 : 0, settingId);
3886
+ this.audit(workId, "setting.favorite-updated", "setting", settingId, {
3887
+ previousFavorite,
3888
+ isFavorite
3889
+ });
3890
+ });
3891
+ return this.getSetting(settingId);
3892
+ }
3861
3893
  updateSetting(settingId, input, source = "manual", sourceRef = null, changeNote = "", expectedVersionNo) {
3862
3894
  const current = this.getSetting(settingId);
3863
3895
  const content = input.content ?? String(current.content);
@@ -3892,6 +3924,7 @@ export class Store {
3892
3924
  tags: json(requiredString(row, "tags_json"), []),
3893
3925
  status: requiredString(row, "status"),
3894
3926
  locked: booleanValue(row, "locked"),
3927
+ isFavorite: booleanValue(row, "is_favorite"),
3895
3928
  evidence: json(requiredString(row, "evidence_json"), []),
3896
3929
  scope: json(requiredString(row, "scope_json"), {}),
3897
3930
  authorNote: requiredString(row, "author_note"),
@@ -4205,14 +4238,14 @@ export class Store {
4205
4238
  }
4206
4239
  listOrganizations(workId, includeMarkdown = true) {
4207
4240
  this.getWork(workId);
4208
- const rows = this.db.all("SELECT * FROM organizations WHERE work_id = ? ORDER BY name", workId);
4241
+ const rows = this.db.all("SELECT * FROM organizations WHERE work_id = ? ORDER BY is_favorite DESC, name", workId);
4209
4242
  const batch = this.organizationListBatch(rows);
4210
4243
  return rows.map((row) => this.mapOrganization(row, includeMarkdown, batch));
4211
4244
  }
4212
4245
  listOrganizationsPage(workId, pagination, includeMarkdown = true) {
4213
4246
  this.getWork(workId);
4214
4247
  const page = paginationSql(pagination);
4215
- const rows = this.db.all(`SELECT * FROM organizations WHERE work_id = ? ORDER BY name${page.sql}`, workId, ...page.params);
4248
+ const rows = this.db.all(`SELECT * FROM organizations WHERE work_id = ? ORDER BY is_favorite DESC, name${page.sql}`, workId, ...page.params);
4216
4249
  const batch = this.organizationListBatch(rows);
4217
4250
  return paginated(rows.map((row) => this.mapOrganization(row, includeMarkdown, batch)), pagination);
4218
4251
  }
@@ -4222,6 +4255,21 @@ export class Store {
4222
4255
  throw notFound("组织");
4223
4256
  return this.mapOrganization(row);
4224
4257
  }
4258
+ setOrganizationFavorite(organizationId, isFavorite) {
4259
+ const organization = this.db.get("SELECT id, work_id, is_favorite FROM organizations WHERE id = ?", organizationId);
4260
+ if (!organization)
4261
+ throw notFound("组织");
4262
+ const workId = requiredString(organization, "work_id");
4263
+ const previousFavorite = booleanValue(organization, "is_favorite");
4264
+ this.db.transaction(() => {
4265
+ this.db.run("UPDATE organizations SET is_favorite = ? WHERE id = ?", isFavorite ? 1 : 0, organizationId);
4266
+ this.audit(workId, "organization.favorite-updated", "organization", organizationId, {
4267
+ previousFavorite,
4268
+ isFavorite
4269
+ });
4270
+ });
4271
+ return this.getOrganization(organizationId);
4272
+ }
4225
4273
  updateOrganization(organizationId, input, source = "manual", sourceRef = null, changeNote = "", expectedVersionNo) {
4226
4274
  const current = this.getOrganization(organizationId);
4227
4275
  const workId = String(current.workId);
@@ -4349,6 +4397,7 @@ export class Store {
4349
4397
  name: requiredString(row, "name"),
4350
4398
  description: requiredString(row, "description"),
4351
4399
  isDissolved: booleanValue(row, "is_dissolved"),
4400
+ isFavorite: booleanValue(row, "is_favorite"),
4352
4401
  ...(includeMarkdown
4353
4402
  ? { settings, settingsMarkdown: settingsMarkdownFromList(settings), settingsSections }
4354
4403
  : { settings: [], settingsCount: settingsSections.length }),
@@ -4467,14 +4516,14 @@ export class Store {
4467
4516
  }
4468
4517
  listCharacters(workId, includeProfileSections = false, includeMerged = false, includeRaceMarkdown = true) {
4469
4518
  this.getWork(workId);
4470
- return this.db.all(`SELECT * FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"} ORDER BY name`, workId)
4519
+ return this.db.all(`SELECT * FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"} ORDER BY is_favorite DESC, name`, workId)
4471
4520
  .map((row) => this.mapCharacter(row, includeProfileSections, includeRaceMarkdown));
4472
4521
  }
4473
4522
  listCharactersPage(workId, pagination, includeProfileSections = false, includeMerged = false, includeRaceMarkdown = true) {
4474
4523
  this.getWork(workId);
4475
4524
  const page = paginationSql(pagination);
4476
4525
  const count = this.db.get(`SELECT COUNT(*) AS count FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"}`, workId);
4477
- const rows = this.db.all(`SELECT * FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"} ORDER BY name${page.sql}`, workId, ...page.params);
4526
+ const rows = this.db.all(`SELECT * FROM characters WHERE work_id = ?${includeMerged ? "" : " AND merged_into_character_id IS NULL"} ORDER BY is_favorite DESC, name${page.sql}`, workId, ...page.params);
4478
4527
  return paginated(rows.map((row) => this.mapCharacter(row, includeProfileSections, includeRaceMarkdown)), pagination, Number(count?.count ?? 0));
4479
4528
  }
4480
4529
  mapCharacterProfileSection(row) {
@@ -4955,6 +5004,26 @@ export class Store {
4955
5004
  throw notFound("角色");
4956
5005
  return this.mapCharacter(row);
4957
5006
  }
5007
+ setCharacterFavorite(characterId, isFavorite) {
5008
+ const character = this.db.get("SELECT id, work_id, is_favorite, merged_into_character_id FROM characters WHERE id = ?", characterId);
5009
+ if (!character)
5010
+ throw notFound("角色");
5011
+ if (optionalString(character, "merged_into_character_id")) {
5012
+ throw new AppError(409, "CHARACTER_ALREADY_MERGED", "已合并角色不能收藏");
5013
+ }
5014
+ const workId = requiredString(character, "work_id");
5015
+ const previousFavorite = booleanValue(character, "is_favorite");
5016
+ if (previousFavorite === isFavorite)
5017
+ return this.getCharacter(characterId);
5018
+ this.db.transaction(() => {
5019
+ this.db.run("UPDATE characters SET is_favorite = ? WHERE id = ?", isFavorite ? 1 : 0, characterId);
5020
+ this.audit(workId, "character.favorite-updated", "character", characterId, {
5021
+ previousFavorite,
5022
+ isFavorite
5023
+ });
5024
+ });
5025
+ return this.getCharacter(characterId);
5026
+ }
4958
5027
  getCharacterAvatar(characterId) {
4959
5028
  const character = this.db.get("SELECT id FROM characters WHERE id = ?", characterId);
4960
5029
  if (!character)
@@ -5229,6 +5298,7 @@ export class Store {
5229
5298
  profileSectionCount,
5230
5299
  currentState: json(requiredString(row, "current_state_json"), {}),
5231
5300
  isDead: booleanValue(row, "is_dead"),
5301
+ isFavorite: booleanValue(row, "is_favorite"),
5232
5302
  avatarUrl: avatarSha256
5233
5303
  ? `/api/characters/${encodeURIComponent(characterId)}/avatar?v=${encodeURIComponent(avatarSha256)}`
5234
5304
  : null,
@@ -6053,6 +6123,7 @@ export class Store {
6053
6123
  totalMessageCount,
6054
6124
  warningPending: Boolean(optionalString(conversation, "context_warning_at")),
6055
6125
  injectedEntities: parseAiInjectedEntities(optionalString(conversation, "injected_entities_json") ?? EMPTY_AI_INJECTED_ENTITIES),
6126
+ scenePin: normalizeRoleplayScenePin(json(optionalString(conversation, "scene_pin_json") ?? "{}", {})),
6056
6127
  messages: rows.filter((message) => requiredString(message, "id") !== excludeMessageId)
6057
6128
  .map((message) => ({
6058
6129
  id: requiredString(message, "id"),
@@ -6134,6 +6205,16 @@ export class Store {
6134
6205
  const refreshed = this.db.get("SELECT system_clock_text FROM ai_conversations WHERE id = ?", conversationId);
6135
6206
  return (optionalString(refreshed ?? {}, "system_clock_text") ?? clock).trim() || clock;
6136
6207
  }
6208
+ setAiConversationScenePin(conversationId, workId, pin) {
6209
+ const conversation = this.db.get("SELECT work_id FROM ai_conversations WHERE id = ?", conversationId);
6210
+ if (!conversation)
6211
+ throw notFound("AI 对话");
6212
+ if (requiredString(conversation, "work_id") !== workId)
6213
+ throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
6214
+ const normalized = normalizeRoleplayScenePin(pin);
6215
+ this.db.run("UPDATE ai_conversations SET scene_pin_json = ?, updated_at = ? WHERE id = ?", JSON.stringify(normalized), now(), conversationId);
6216
+ return normalized;
6217
+ }
6137
6218
  listCharacterNameEntries(workId) {
6138
6219
  this.getWork(workId);
6139
6220
  return this.db.all(`SELECT character_id, normalized_name, display_name, kind FROM character_names
@@ -6577,10 +6658,11 @@ export class Store {
6577
6658
  const injectedEntitiesJson = optionalString(conversation, "injected_entities_json")
6578
6659
  ?? JSON.stringify(EMPTY_AI_INJECTED_ENTITIES);
6579
6660
  const systemClockText = optionalString(conversation, "system_clock_text") ?? "";
6661
+ const scenePinJson = optionalString(conversation, "scene_pin_json") ?? "{}";
6580
6662
  this.db.transaction(() => {
6581
- this.db.run("INSERT INTO ai_conversations (id, work_id, roleplay_character_id, roleplay_user_character_id, task_type, context_scope_json, title, compacted_summary, compacted_message_count, agent_tools_json, injected_entities_json, system_clock_text, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", forkId, workId, optionalString(conversation, "roleplay_character_id"), optionalString(conversation, "roleplay_user_character_id"), optionalString(conversation, "task_type"), optionalString(conversation, "context_scope_json"), title.slice(0, 200), forkSummary, forkCompactedCount, conversation.agent_tools_json == null
6663
+ this.db.run("INSERT INTO ai_conversations (id, work_id, roleplay_character_id, roleplay_user_character_id, task_type, context_scope_json, title, compacted_summary, compacted_message_count, agent_tools_json, injected_entities_json, system_clock_text, scene_pin_json, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", forkId, workId, optionalString(conversation, "roleplay_character_id"), optionalString(conversation, "roleplay_user_character_id"), optionalString(conversation, "task_type"), optionalString(conversation, "context_scope_json"), title.slice(0, 200), forkSummary, forkCompactedCount, conversation.agent_tools_json == null
6582
6664
  ? JSON.stringify(normalizeWorkAgentTools(this.getWorkAiSettings(workId).agentTools))
6583
- : String(conversation.agent_tools_json), injectedEntitiesJson, systemClockText, timestamp, timestamp, currentRequestActor()?.userId ?? null);
6665
+ : String(conversation.agent_tools_json), injectedEntitiesJson, systemClockText, scenePinJson, timestamp, timestamp, currentRequestActor()?.userId ?? null);
6584
6666
  for (const message of messages.slice(0, targetIndex + 1)) {
6585
6667
  const role = requiredString(message, "role");
6586
6668
  const inheritedMetadata = json(requiredString(message, "metadata_json"), {});
@@ -6647,7 +6729,7 @@ export class Store {
6647
6729
  title: requiredString(row, "title"),
6648
6730
  isFavorite: booleanValue(row, "is_favorite"),
6649
6731
  messageCount: numberValue(row, "message_count"),
6650
- preview: requiredString(row, "preview"),
6732
+ preview: roleplayUserTurnDisplayText(requiredString(row, "preview")),
6651
6733
  compactedMessageCount: numberValue(row, "compacted_message_count"),
6652
6734
  hasCompactedSummary: Boolean(requiredString(row, "compacted_summary")),
6653
6735
  contextWarningPending: Boolean(optionalString(row, "context_warning_at")),
@@ -6655,6 +6737,7 @@ export class Store {
6655
6737
  ...(lockedModelId ? { modelId: lockedModelId } : {}),
6656
6738
  ...(hasImageAttachments ? { hasImageAttachments: true, modelLockedByImage: true } : {}),
6657
6739
  contextScope: json(optionalString(row, "context_scope_json") ?? "", { type: "none" }),
6740
+ scenePin: normalizeRoleplayScenePin(json(optionalString(row, "scene_pin_json") ?? "{}", {})),
6658
6741
  roleplayCharacter: roleplayCharacter ? {
6659
6742
  id: requiredString(roleplayCharacter, "id"),
6660
6743
  name: requiredString(roleplayCharacter, "name"),