@musnows/scriverse 0.8.1 → 0.8.3

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.
@@ -561,6 +561,8 @@ input[type="checkbox"][hidden] { display: none; }
561
561
  .panel-resize-handle:hover::after, .panel-resize-handle:focus-visible::after, body.is-panel-resizing .panel-resize-handle::after { background: var(--accent); }
562
562
  body.is-panel-resizing { cursor: col-resize; user-select: none; }
563
563
  .app-shell.left-panel-collapsed { --left-panel-width: 42px !important; }.app-shell.ai-panel-collapsed { --ai-panel-width: 42px !important; }
564
+ .app-shell.phone-client.ai-panel-collapsed:not(.ai-workspace-mode) { --ai-panel-width: 0px !important; }
565
+ .app-shell.phone-client.ai-panel-collapsed:not(.ai-workspace-mode) .ai-panel { display: none; }
564
566
  .app-shell.left-panel-collapsed .left-panel, .app-shell.ai-panel-collapsed .ai-panel { padding: 0; overflow: hidden; }
565
567
  .app-shell.left-panel-collapsed .left-panel > .panel-resize-handle,
566
568
  .app-shell.left-panel-collapsed .left-panel-body > :not(.left-actions),
@@ -1415,7 +1417,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1415
1417
  .chapter-line-number { position: absolute; left: 0; box-sizing: border-box; display: flex; width: 100%; align-items: flex-start; justify-content: center; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; font-variant-numeric: tabular-nums; text-align: center; }
1416
1418
  .chapter-line-number-select { position: relative; display: flex; width: 100%; height: 100%; align-items: flex-start; justify-content: center; padding-inline: 3px; border: 0; background: transparent; color: inherit; font: inherit; font-variant-numeric: tabular-nums; cursor: pointer; }
1417
1419
  .chapter-line-number-select:focus-visible { z-index: 1; outline: 1px solid var(--accent); outline-offset: -1px; }
1418
- .chapter-line-annotation-count { position: absolute; top: -2px; right: 0; z-index: 2; display: grid; min-width: 14px; height: 14px; place-items: center; padding: 0 3px; border: 1px solid var(--panel); border-radius: 999px; background: var(--accent); color: var(--paper); font-family: var(--font-latin), monospace; font-size: 9px; font-weight: 700; line-height: 1; cursor: pointer; box-shadow: 0 1px 3px rgba(49,42,32,.16); }
1420
+ .chapter-line-annotation-count { position: absolute; top: -4px; right: -10px; z-index: 2; display: grid; min-width: 14px; height: 14px; place-items: center; padding: 0 3px; border: 1px solid var(--panel); border-radius: 999px; background: var(--accent); color: var(--paper); font-family: var(--font-latin), monospace; font-size: 9px; font-weight: 700; line-height: 1; cursor: pointer; box-shadow: 0 1px 3px rgba(49,42,32,.16); }
1419
1421
  .chapter-line-annotation-count:hover, .chapter-line-annotation-count:focus-visible { background: var(--accent-dark); }
1420
1422
  .chapter-line-annotation-count:focus-visible { outline: 1px solid var(--accent-dark); outline-offset: 1px; }
1421
1423
  .chapter-line-number:hover { background: rgba(139,61,44,.08); color: var(--accent-dark); }
@@ -2655,6 +2657,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2655
2657
  .message-body .markdown-image img { display: block; max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: 5px; background: var(--surface-soft); }
2656
2658
  .message-body .markdown-image small { color: var(--muted); font-size: 9px; line-height: 1.45; text-align: center; }
2657
2659
  .message-meta { margin-top: 8px; color: var(--muted); font-size: 9px; }
2660
+ .message-meta .ai-stream-character-count { display: inline-block; min-width: 6ch; font-variant-numeric: tabular-nums; text-align: right; }
2658
2661
  .ai-process-details { margin: 0 0 10px; overflow: hidden; border: 1px solid var(--line); border-radius: 5px; background: color-mix(in srgb, var(--surface-soft) 82%, transparent); }
2659
2662
  .ai-process-details > summary { display: flex; align-items: center; justify-content: space-between; gap: 9px; padding: 8px 10px; color: var(--muted); cursor: pointer; font-size: 10px; list-style: none; }
2660
2663
  .ai-process-details > summary::-webkit-details-marker { display: none; }
@@ -2711,9 +2714,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2711
2714
  .prompt-options { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 7px; }
2712
2715
  .prompt-options select { padding: 6px; color: var(--muted); font-size: 10px; }
2713
2716
  .prompt-options .ai-roleplay-character { min-width: 0; }
2717
+ .prompt-options .ai-roleplay-user-character { grid-column: 1 / -1; }
2714
2718
  .ai-panel.is-roleplaying .ai-roleplay-character { border-color: color-mix(in srgb, var(--accent) 52%, var(--line)); background: color-mix(in srgb, var(--accent) 8%, var(--surface)); color: var(--accent-dark); }
2715
- .prompt-model-field { margin: 0 0 7px; }
2716
- .prompt-model-field select { padding: 6px; color: var(--muted); font-size: 10px; }
2717
2719
  .prompt-composer { position: relative; }
2718
2720
  .ai-mention-menu { position: absolute; z-index: 20; left: 0; right: 0; bottom: calc(100% + 6px); max-height: 230px; overflow-y: auto; padding: 6px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); box-shadow: 0 12px 34px rgba(49,42,32,.18); }
2719
2721
  .ai-mention-option { display: grid; grid-template-columns: 34px minmax(0, 1fr); gap: 8px; width: 100%; padding: 7px 8px; border: 0; border-radius: 4px; background: transparent; text-align: left; }
@@ -2736,13 +2738,24 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2736
2738
  .ai-context-meter b { position: relative; z-index: 1; font-size: 9px; font-weight: 600; }.ai-context-meter.is-warning { --context-meter-color: #bf6a35; }.ai-context-meter.is-danger { --context-meter-color: #ad463c; }.ai-context-meter.is-empty { opacity: .62; }
2737
2739
  .ai-context-popover { position: absolute; right: 0; bottom: calc(100% + 10px); z-index: 40; display: grid; width: min(320px, calc(100vw - 32px)); gap: 12px; padding: 14px; border: 1px solid var(--line); border-radius: 7px; background: var(--paper); color: var(--ink); box-shadow: 0 14px 38px rgba(36, 30, 24, .18); }
2738
2740
  .ai-context-popover.hidden { display: none; }
2739
- .ai-context-popover::after { position: absolute; right: 71px; bottom: -6px; width: 10px; height: 10px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--paper); content: ""; transform: rotate(45deg); }
2741
+ .ai-context-popover::after { position: absolute; right: 87px; bottom: -6px; width: 10px; height: 10px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--paper); content: ""; transform: rotate(45deg); }
2740
2742
  .ai-context-popover-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
2741
2743
  .ai-context-popover-header > div { display: grid; gap: 3px; min-width: 0; }
2742
2744
  .ai-context-popover-header strong { font-size: 12px; }
2743
2745
  .ai-context-popover-header small { color: var(--muted); font: 9px/1.4 var(--font-latin), var(--font-cjk), sans-serif; }
2744
2746
  .ai-context-popover-close { flex: 0 0 auto; width: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); font: 17px/1 var(--font-latin), sans-serif; }
2745
2747
  .ai-context-popover-close:hover, .ai-context-popover-close:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: 0; }
2748
+ .ai-model-picker { display: grid; flex: 0 0 32px; place-items: center; width: 32px; min-width: 32px; min-height: 32px; height: 32px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--muted); }
2749
+ .ai-model-picker:hover, .ai-model-picker:focus-visible, .ai-model-picker[aria-expanded="true"] { border-color: color-mix(in srgb, var(--accent) 56%, var(--line)); background: color-mix(in srgb, var(--accent) 8%, var(--surface)); color: var(--accent-dark); outline: 0; }
2750
+ .ai-model-picker:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); }
2751
+ .ai-model-picker:disabled { cursor: wait; opacity: .56; }
2752
+ .ai-model-picker-icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
2753
+ .ai-model-popover { position: absolute; right: 0; bottom: calc(100% + 10px); z-index: 41; display: grid; width: min(280px, calc(100vw - 32px)); gap: 7px; padding: 12px; border: 1px solid var(--line); border-radius: 7px; background: var(--paper); color: var(--ink); box-shadow: 0 14px 38px rgba(36, 30, 24, .18); }
2754
+ .ai-model-popover.hidden { display: none; }
2755
+ .ai-model-popover::after { position: absolute; right: 49px; bottom: -6px; width: 10px; height: 10px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--paper); content: ""; transform: rotate(45deg); }
2756
+ .ai-model-popover > label { color: var(--muted); font-size: 10px; font-weight: 600; }
2757
+ .ai-model-popover select { width: 100%; min-width: 0; padding: 7px; color: var(--ink); font-size: 10px; }
2758
+ .ai-model-popover select:focus-visible { border-color: var(--accent); outline: 0; box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 14%, transparent); }
2746
2759
  .ai-context-distribution { display: grid; gap: 10px; }
2747
2760
  .ai-context-distribution-row { display: grid; gap: 5px; }
2748
2761
  .ai-context-distribution-label { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font: 10px/1.3 var(--font-latin), sans-serif; }
@@ -3311,6 +3324,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3311
3324
  .vditor-editor-host.vditor--dark { --border-color: var(--line); --panel-background-color: var(--surface); --toolbar-background-color: var(--surface-soft); --toolbar-icon-color: var(--muted); --toolbar-icon-hover-color: var(--accent); --textarea-background-color: var(--surface); --textarea-text-color: var(--ink); --heading-border-color: var(--line); --blockquote-color: var(--muted); }
3312
3325
  .vditor-editor-host .vditor-toolbar { display: flex; align-items: center; flex: 0 0 auto; }
3313
3326
  .vditor-editor-host .markdown-word-count { display: inline-flex; align-items: center; align-self: center; min-height: 28px; margin-left: 24px; padding: 0 10px; color: var(--muted); font: 10px/1 var(--font-latin), var(--font-cjk), sans-serif; white-space: nowrap; }
3327
+ .vditor-editor-host .markdown-word-count-value { display: inline-block; min-width: 6ch; font-variant-numeric: tabular-nums; text-align: right; }
3314
3328
  .vditor-editor-host .vditor-content { position: relative; }
3315
3329
  .vditor-editor-host .vditor-line-numbers { display: none; position: absolute; z-index: 1; inset: 0 auto 0 0; width: 42px; padding: 0; overflow: hidden; border-right: 1px solid var(--line); background: color-mix(in srgb, var(--surface-soft) 88%, transparent); color: var(--muted); font: 12px/1.75 var(--font-latin), monospace; text-align: center; user-select: none; pointer-events: none; will-change: transform; }
3316
3330
  .vditor-editor-host.vditor.show-line-numbers .vditor-line-numbers { display: block; }
@@ -3999,7 +4013,7 @@ body { font-size: var(--ui-font-size); }
3999
4013
  .guard-card strong, .guard-card p, .message-actions button { font-size: calc(10px * var(--ai-font-scale)); }
4000
4014
  .ai-context-warning strong { font-size: calc(11px * var(--ai-font-scale)); }
4001
4015
  .ai-context-warning p, .ai-context-warning button { font-size: calc(9px * var(--ai-font-scale)); }
4002
- .prompt-options select, .prompt-model-field select, .ai-mention-option strong, .ai-mention-empty { font-size: calc(10px * var(--ai-font-scale)); }
4016
+ .prompt-options select, .ai-model-popover > label, .ai-model-popover select, .ai-mention-option strong, .ai-mention-empty { font-size: calc(10px * var(--ai-font-scale)); }
4003
4017
  .ai-mention-option small { font-size: calc(8px * var(--ai-font-scale)); }
4004
4018
  .ai-prompt { font-size: var(--ai-font-size); }
4005
4019
  .ai-prompt-reference { font-size: calc(10px * var(--ai-font-scale)); }
package/dist/store.js CHANGED
@@ -5561,6 +5561,7 @@ export class Store {
5561
5561
  return {
5562
5562
  workId,
5563
5563
  roleplayCharacterId: optionalString(conversation, "roleplay_character_id"),
5564
+ roleplayUserCharacterId: optionalString(conversation, "roleplay_user_character_id"),
5564
5565
  summary: requiredString(conversation, "compacted_summary"),
5565
5566
  compactedMessageCount,
5566
5567
  totalMessageCount,
@@ -5704,14 +5705,27 @@ export class Store {
5704
5705
  this.audit(workId, "ai-conversation.deleted", "ai-conversation", conversationId, {});
5705
5706
  });
5706
5707
  }
5707
- setAiConversationRoleplayCharacter(conversationId, characterId) {
5708
+ setAiConversationRoleplayCharacter(conversationId, characterId, userCharacterId) {
5708
5709
  const conversation = this.db.get("SELECT * FROM ai_conversations WHERE id = ?", conversationId);
5709
5710
  if (!conversation)
5710
5711
  throw notFound("AI 对话");
5711
5712
  const workId = requiredString(conversation, "work_id");
5712
5713
  const previousCharacterId = optionalString(conversation, "roleplay_character_id");
5713
- if (previousCharacterId === characterId)
5714
+ const previousUserCharacterId = optionalString(conversation, "roleplay_user_character_id");
5715
+ if (!characterId && userCharacterId) {
5716
+ throw new AppError(400, "ROLEPLAY_USER_CHARACTER_REQUIRES_AI", "请先选择 AI 扮演的角色");
5717
+ }
5718
+ const nextUserCharacterId = !characterId
5719
+ ? null
5720
+ : userCharacterId === undefined
5721
+ ? previousCharacterId === characterId ? previousUserCharacterId : null
5722
+ : userCharacterId;
5723
+ if (characterId && characterId === nextUserCharacterId) {
5724
+ throw new AppError(400, "ROLEPLAY_CHARACTER_SAME_AS_USER", "AI 扮演角色与用户扮演角色不能相同");
5725
+ }
5726
+ if (previousCharacterId === characterId && previousUserCharacterId === nextUserCharacterId) {
5714
5727
  return this.getAiConversationSummary(conversationId);
5728
+ }
5715
5729
  const messageCount = Number(this.db.get("SELECT COUNT(*) AS count FROM ai_conversation_messages WHERE conversation_id = ?", conversationId)?.count ?? 0);
5716
5730
  if (messageCount > 0) {
5717
5731
  throw new AppError(409, previousCharacterId ? "ROLEPLAY_CHARACTER_LOCKED" : "ROLEPLAY_CONVERSATION_STARTED", previousCharacterId ? "角色扮演对话开始后不能退出模式或更换角色卡" : "当前对话已经开始,不能中途切换为角色扮演");
@@ -5725,11 +5739,22 @@ export class Store {
5725
5739
  throw new AppError(409, "ROLEPLAY_CHARACTER_MERGED", "已合并角色不能用于角色扮演");
5726
5740
  }
5727
5741
  }
5742
+ if (nextUserCharacterId) {
5743
+ const userCharacter = this.getCharacter(nextUserCharacterId);
5744
+ if (String(userCharacter.workId) !== workId) {
5745
+ throw new AppError(400, "ROLEPLAY_USER_CHARACTER_WORK_MISMATCH", "用户扮演的角色不属于当前作品");
5746
+ }
5747
+ if (userCharacter.mergedIntoCharacterId) {
5748
+ throw new AppError(409, "ROLEPLAY_USER_CHARACTER_MERGED", "已合并角色不能用于关系扮演");
5749
+ }
5750
+ }
5728
5751
  this.db.transaction(() => {
5729
- this.db.run("UPDATE ai_conversations SET roleplay_character_id = ?, task_type = CASE WHEN ? IS NOT NULL THEN 'roleplay' ELSE task_type END, updated_at = ? WHERE id = ?", characterId, characterId, now(), conversationId);
5752
+ this.db.run("UPDATE ai_conversations SET roleplay_character_id = ?, roleplay_user_character_id = ?, task_type = CASE WHEN ? IS NOT NULL THEN 'roleplay' ELSE task_type END, updated_at = ? WHERE id = ?", characterId, nextUserCharacterId, characterId, now(), conversationId);
5730
5753
  this.audit(workId, "ai-conversation.roleplay-updated", "ai-conversation", conversationId, {
5731
5754
  previousCharacterId,
5732
- characterId
5755
+ characterId,
5756
+ previousUserCharacterId,
5757
+ userCharacterId: nextUserCharacterId
5733
5758
  });
5734
5759
  });
5735
5760
  return this.getAiConversationSummary(conversationId);
@@ -5748,7 +5773,7 @@ export class Store {
5748
5773
  throw new AppError(409, "AI_CONVERSATION_TASK_LOCKED", "对话开始后不能切换任务类型");
5749
5774
  }
5750
5775
  this.db.transaction(() => {
5751
- this.db.run("UPDATE ai_conversations SET task_type = ?, roleplay_character_id = CASE WHEN ? = 'roleplay' THEN roleplay_character_id ELSE NULL END, updated_at = ? WHERE id = ?", taskType, taskType, now(), conversationId);
5776
+ this.db.run("UPDATE ai_conversations SET task_type = ?, roleplay_character_id = CASE WHEN ? = 'roleplay' THEN roleplay_character_id ELSE NULL END, roleplay_user_character_id = CASE WHEN ? = 'roleplay' THEN roleplay_user_character_id ELSE NULL END, updated_at = ? WHERE id = ?", taskType, taskType, taskType, now(), conversationId);
5752
5777
  this.audit(workId, "ai-conversation.task-type-updated", "ai-conversation", conversationId, {
5753
5778
  previousTaskType,
5754
5779
  taskType
@@ -6041,7 +6066,7 @@ export class Store {
6041
6066
  ?? JSON.stringify(EMPTY_AI_INJECTED_ENTITIES);
6042
6067
  const systemClockText = optionalString(conversation, "system_clock_text") ?? "";
6043
6068
  this.db.transaction(() => {
6044
- this.db.run("INSERT INTO ai_conversations (id, work_id, roleplay_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, "task_type"), optionalString(conversation, "context_scope_json"), title.slice(0, 200), forkSummary, forkCompactedCount, conversation.agent_tools_json == null
6069
+ 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
6045
6070
  ? JSON.stringify(normalizeWorkAgentTools(this.getWorkAiSettings(workId).agentTools))
6046
6071
  : String(conversation.agent_tools_json), injectedEntitiesJson, systemClockText, timestamp, timestamp, currentRequestActor()?.userId ?? null);
6047
6072
  for (const message of messages.slice(0, targetIndex + 1)) {
@@ -6084,6 +6109,7 @@ export class Store {
6084
6109
  }
6085
6110
  mapAiConversation(row) {
6086
6111
  const roleplayCharacterId = optionalString(row, "roleplay_character_id");
6112
+ const roleplayUserCharacterId = optionalString(row, "roleplay_user_character_id");
6087
6113
  const firstUserMessage = this.db.get(`SELECT metadata_json FROM ai_conversation_messages
6088
6114
  WHERE conversation_id = ? AND role = 'user'
6089
6115
  ORDER BY created_at, rowid
@@ -6097,6 +6123,9 @@ export class Store {
6097
6123
  const roleplayCharacter = roleplayCharacterId
6098
6124
  ? this.db.get("SELECT id, name, code FROM characters WHERE id = ? AND work_id = ?", roleplayCharacterId, requiredString(row, "work_id"))
6099
6125
  : undefined;
6126
+ const roleplayUserCharacter = roleplayCharacterId && roleplayUserCharacterId
6127
+ ? this.db.get("SELECT id, name, code FROM characters WHERE id = ? AND work_id = ?", roleplayUserCharacterId, requiredString(row, "work_id"))
6128
+ : undefined;
6100
6129
  return {
6101
6130
  id: requiredString(row, "id"),
6102
6131
  workId: requiredString(row, "work_id"),
@@ -6115,6 +6144,11 @@ export class Store {
6115
6144
  name: requiredString(roleplayCharacter, "name"),
6116
6145
  code: requiredString(roleplayCharacter, "code")
6117
6146
  } : null,
6147
+ roleplayUserCharacter: roleplayUserCharacter ? {
6148
+ id: requiredString(roleplayUserCharacter, "id"),
6149
+ name: requiredString(roleplayUserCharacter, "name"),
6150
+ code: requiredString(roleplayUserCharacter, "code")
6151
+ } : null,
6118
6152
  agentTools: row.agent_tools_json == null || row.agent_tools_json === undefined
6119
6153
  ? null
6120
6154
  : normalizeWorkAgentTools(row.agent_tools_json),