@musnows/scriverse 0.8.7 → 0.9.0

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.
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
12
  <link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
13
- <link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1">
13
+ <link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -82,7 +82,7 @@
82
82
  <div id="presence-list" class="presence-list" aria-live="polite"></div>
83
83
  </section>
84
84
  </div>
85
- <button id="account-button" class="account-button" type="button" aria-haspopup="menu" aria-expanded="false"><span id="account-avatar" class="user-avatar account-avatar" aria-hidden="true"><span class="user-avatar-fallback">作</span></span><span id="account-name">账户</span></button>
85
+ <button id="account-button" class="account-button" type="button" aria-label="账户" aria-haspopup="menu" aria-expanded="false"><span class="account-avatar-frame"><span id="account-avatar" class="user-avatar account-avatar" aria-hidden="true"><span class="user-avatar-fallback">作</span></span><span id="account-admin-mark" class="account-admin-mark hidden" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M8 1.8 13 3.7v3.7c0 3.1-2 5.6-5 6.8-3-1.2-5-3.7-5-6.8V3.7L8 1.8Z"/><path d="m5.5 7.8 1.6 1.6 3.5-3.5"/></svg></span></span><span class="account-identity"><span id="account-name">账户</span></span></button>
86
86
  <div id="account-menu" class="account-menu hidden" role="menu">
87
87
  <strong id="account-menu-name" class="account-menu-name"><span id="account-menu-display-name">账户</span><span id="account-menu-username" class="account-menu-username">@account</span></strong><small id="account-menu-role">普通用户</small>
88
88
  <button id="onboarding-menu-button" type="button" role="menuitem">功能导览</button>
@@ -386,7 +386,18 @@
386
386
  </div>
387
387
  <div class="prompt-composer">
388
388
  <div id="ai-image-attachments" class="ai-image-attachments hidden" aria-label="待发送的图片附件" aria-live="polite"></div>
389
- <button id="ai-attachment-button" class="ai-attachment-button hidden" type="button" aria-label="添加图片附件" title="添加图片附件"><svg class="ai-image-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="4.5" width="17" height="15" rx="2"></rect><circle cx="9" cy="10" r="1.5"></circle><path d="m5.5 17 4.4-4.4 3.3 3.1 2.2-2.2 3.1 3.5"></path></svg></button>
389
+ <section id="ai-scene-panel" class="ai-scene-panel hidden" aria-label="旁白与场景">
390
+ <div class="ai-scene-pin-fields">
391
+ <label for="ai-scene-location">地点<input id="ai-scene-location" maxlength="200" autocomplete="off"></label>
392
+ <label for="ai-scene-present">在场人物<input id="ai-scene-present" maxlength="500" autocomplete="off"></label>
393
+ <label for="ai-scene-time">故事时间<input id="ai-scene-time" maxlength="200" autocomplete="off"></label>
394
+ </div>
395
+ <label for="ai-scene-direction">旁白 / 场景<textarea id="ai-scene-direction" rows="3" maxlength="20000" placeholder="写下本轮旁白或场景推进,会随下一条台词一起发送"></textarea></label>
396
+ </section>
397
+ <div class="prompt-composer-leading">
398
+ <button id="ai-attachment-button" class="ai-attachment-button hidden" type="button" aria-label="添加图片附件" title="添加图片附件"><svg class="ai-image-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="4.5" width="17" height="15" rx="2"></rect><circle cx="9" cy="10" r="1.5"></circle><path d="m5.5 17 4.4-4.4 3.3 3.1 2.2-2.2 3.1 3.5"></path></svg></button>
399
+ <button id="ai-scene-button" class="ai-scene-button hidden" type="button" aria-label="旁白与场景" title="旁白与场景" aria-expanded="false" aria-controls="ai-scene-panel"><svg class="ai-scene-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="4.5" y="6.5" width="15" height="13" rx="1.5"></rect><path d="M8 10.5h8M8 14h5.5"></path></svg></button>
400
+ </div>
390
401
  <input id="ai-attachment-input" class="ai-attachment-input" type="file" accept="image/png,image/jpeg,.jpg,.jpeg" multiple aria-label="选择图片附件">
391
402
  <div id="ai-prompt" class="ai-prompt" contenteditable="true" role="textbox" aria-multiline="true" aria-autocomplete="list" aria-controls="ai-mention-menu" aria-haspopup="listbox" aria-expanded="false" aria-keyshortcuts="Enter" title="Enter 发送,Shift+Enter 换行" data-placeholder="告诉 AI 你想讨论或修改什么……"></div>
392
403
  <div id="ai-mention-menu" class="ai-mention-menu hidden" role="listbox" aria-label="引用角色、设定、章节或上下文能力"></div>
@@ -556,7 +567,7 @@
556
567
  <form id="dynamic-form" method="dialog">
557
568
  <div class="dialog-header">
558
569
  <div><span id="dialog-eyebrow" class="eyebrow">新增</span><h2 id="dialog-title">创建记录</h2><input id="dialog-title-input" class="dialog-title-input hidden" type="text" autocomplete="off" aria-label="记录标题"><p id="dialog-meta" class="dialog-header-meta hidden"></p></div>
559
- <button class="dialog-close" value="cancel" aria-label="关闭" type="submit">×</button>
570
+ <div class="settings-dialog-header-actions"><div id="dialog-context-actions" class="dialog-context-actions"></div><button class="dialog-close" value="cancel" aria-label="关闭" type="submit">×</button></div>
560
571
  </div>
561
572
  <div id="dialog-fields" class="dialog-fields"></div>
562
573
  <div id="dialog-submit-status" class="dialog-submit-status hidden" role="status" aria-live="polite">
@@ -647,6 +658,7 @@
647
658
  </div>
648
659
  </div>
649
660
  <div class="entity-editor-mode-actions">
661
+ <button id="setting-editor-favorite" class="entity-detail-favorite-button hidden" type="button" aria-pressed="false"></button>
650
662
  <button id="setting-editor-edit" class="ghost-button hidden" type="button">编辑设定</button>
651
663
  <button id="setting-editor-confirm" class="ghost-button hidden" type="button">确认候选</button>
652
664
  <button id="setting-editor-deprecate" class="ghost-button hidden" type="button">弃用</button>
@@ -673,6 +685,7 @@
673
685
  <div class="character-editor-title-row"><h1 id="character-editor-title">新建角色</h1><span id="character-editor-version" class="character-version-badge">新档案</span><span id="character-editor-readonly-badge" class="entity-editor-readonly-badge hidden">只读模式</span></div>
674
686
  </div>
675
687
  <div class="character-editor-header-actions">
688
+ <button id="character-editor-favorite" class="entity-detail-favorite-button hidden" type="button" aria-pressed="false"></button>
676
689
  <button id="character-editor-edit" class="primary-button hidden" type="button">编辑角色</button>
677
690
  <button id="character-history-button" class="ghost-button" type="button" aria-controls="character-history-panel" aria-expanded="false">版本历史</button>
678
691
  <button id="character-merge-button" class="ghost-button hidden" type="button">合并角色</button>
@@ -682,7 +695,7 @@
682
695
  <div class="character-editor-workspace">
683
696
  <nav class="character-editor-nav" role="tablist" aria-label="人物档案编辑分区">
684
697
  <button type="button" role="tab" data-character-editor-tab="basic" aria-selected="true">基础资料<small>名称、种族与归属</small></button>
685
- <button type="button" role="tab" data-character-editor-tab="profile" aria-selected="false">人物档案<small>简介、身份与动机</small></button>
698
+ <button type="button" role="tab" data-character-editor-tab="profile" aria-selected="false">人物档案<small>简介、人设摘要、身份与动机</small></button>
686
699
  <button type="button" role="tab" data-character-editor-tab="settings" aria-selected="false">扩展设定<small>属性与长篇章节</small></button>
687
700
  <button type="button" role="tab" data-character-editor-tab="state" aria-selected="false">状态与约束<small>当前状态与锁定项</small></button>
688
701
  <button type="button" role="tab" data-character-editor-tab="relationships" aria-selected="false">人物关系<small>关联人物与关系关键词</small></button>
@@ -708,6 +721,7 @@
708
721
  </div>
709
722
  <div class="character-editor-header-actions">
710
723
  <span id="knowledge-editor-header-note" class="knowledge-editor-header-note">独立资料页</span>
724
+ <button id="knowledge-editor-favorite" class="entity-detail-favorite-button hidden" type="button" aria-pressed="false"></button>
711
725
  <button id="knowledge-editor-edit" class="primary-button hidden" type="button">编辑档案</button>
712
726
  <button id="knowledge-editor-history" class="ghost-button hidden" type="button">版本历史</button>
713
727
  <button id="knowledge-editor-merge" class="ghost-button hidden" type="button">合并档案</button>
@@ -1077,13 +1091,12 @@
1077
1091
  <form id="password-form" class="account-settings-form password-settings-form" method="post" action="/api/auth/password"><label>当前密码<input name="currentPassword" type="password" autocomplete="current-password" maxlength="200" required></label><label>新密码<input name="newPassword" type="password" autocomplete="new-password" minlength="10" maxlength="200" required></label><label>确认新密码<input name="passwordConfirmation" type="password" autocomplete="new-password" minlength="10" maxlength="200" required></label><button class="primary-button" type="submit">更新密码</button></form>
1078
1092
  </section>
1079
1093
  <section class="account-settings-section" aria-labelledby="api-key-settings-title">
1080
- <header class="account-settings-section-header"><h3 id="api-key-settings-title">CLI API Key</h3><p>用于 CLI 登录。服务端只保存摘要,重置后旧 Key 会立即失效,新 Key 仅显示一次。</p></header>
1094
+ <header class="account-settings-section-header"><h3 id="api-key-settings-title">CLI API Key</h3><p>用于 CLI 登录。完整 Key 不会显示在页面上,可随时复制;重置后旧 Key 会立即失效。</p></header>
1081
1095
  <div class="api-key-settings">
1082
1096
  <p id="api-key-status">正在读取 API Key 状态…</p>
1083
- <button id="api-key-reset-button" class="primary-button" type="button">生成 API Key</button>
1084
- <div id="api-key-result" class="api-key-result hidden">
1085
- <label>请立即保存<input id="api-key-value" readonly spellcheck="false"></label>
1086
- <button id="api-key-copy-button" class="ghost-button" type="button"><svg class="message-action-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></svg>复制</button>
1097
+ <div class="api-key-actions">
1098
+ <button id="api-key-copy-button" class="ghost-button" type="button" hidden aria-label="复制 API Key"><svg class="message-action-icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></svg>复制</button>
1099
+ <button id="api-key-reset-button" class="primary-button" type="button">生成 API Key</button>
1087
1100
  </div>
1088
1101
  </div>
1089
1102
  </section>
@@ -1245,6 +1258,6 @@
1245
1258
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1246
1259
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
1247
1260
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
1248
- <script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v1&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v1&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1"></script>
1261
+ <script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v1&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1"></script>
1249
1262
  </body>
1250
1263
  </html>
@@ -0,0 +1,100 @@
1
+ const SCENE_DIRECTION_TAG = "scene_direction";
2
+ const USER_MESSAGE_TAG = "user_message";
3
+ const SCENE_PIN_LOCATION_MAX = 200;
4
+ const SCENE_PIN_PRESENT_MAX = 500;
5
+ const SCENE_PIN_TIME_MAX = 200;
6
+
7
+ function escapeXmlText(text) {
8
+ return String(text ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;");
9
+ }
10
+
11
+ function unescapeXmlText(text) {
12
+ return String(text ?? "").replaceAll("&lt;", "<").replaceAll("&amp;", "&");
13
+ }
14
+
15
+ function wrapRegion(tag, body, escape = true) {
16
+ const trimmed = String(body ?? "").trim();
17
+ if (!trimmed) return "";
18
+ const content = escape ? escapeXmlText(trimmed) : trimmed;
19
+ return `<${tag}>\n${content}\n</${tag}>`;
20
+ }
21
+
22
+ function unwrapRegion(content, tag) {
23
+ const open = `<${tag}>`;
24
+ const close = `</${tag}>`;
25
+ if (!content.startsWith(open)) return null;
26
+ const closeIndex = content.indexOf(close, open.length);
27
+ if (closeIndex < 0) return null;
28
+ const inner = content.slice(open.length, closeIndex).replace(/^\n/u, "").replace(/\n$/u, "");
29
+ const rest = content.slice(closeIndex + close.length).trim();
30
+ return { inner, rest };
31
+ }
32
+
33
+ export function emptyRoleplayScenePin() {
34
+ return { location: "", present: "", timeLabel: "" };
35
+ }
36
+
37
+ export function normalizeRoleplayScenePin(value) {
38
+ const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
39
+ const clip = (raw, maximum) => (typeof raw === "string" ? raw.trim().slice(0, maximum) : "");
40
+ return {
41
+ location: clip(source.location, SCENE_PIN_LOCATION_MAX),
42
+ present: clip(source.present, SCENE_PIN_PRESENT_MAX),
43
+ timeLabel: clip(source.timeLabel, SCENE_PIN_TIME_MAX)
44
+ };
45
+ }
46
+
47
+ export function roleplayScenePinHasContent(pin) {
48
+ const normalized = normalizeRoleplayScenePin(pin);
49
+ return Boolean(normalized.location || normalized.present || normalized.timeLabel);
50
+ }
51
+
52
+ export function formatRoleplayScenePinText(pin) {
53
+ const normalized = normalizeRoleplayScenePin(pin);
54
+ const lines = [];
55
+ if (normalized.location) lines.push(`地点:${normalized.location}`);
56
+ if (normalized.present) lines.push(`在场:${normalized.present}`);
57
+ if (normalized.timeLabel) lines.push(`故事时间:${normalized.timeLabel}`);
58
+ return lines.join("\n");
59
+ }
60
+
61
+ export function composeRoleplayCurrentUserTurn(sceneDirection, userMessage) {
62
+ return [
63
+ wrapRegion(SCENE_DIRECTION_TAG, sceneDirection, true),
64
+ wrapRegion(USER_MESSAGE_TAG, userMessage, false)
65
+ ].filter(Boolean).join("\n\n");
66
+ }
67
+
68
+ export function composeRoleplayStoredUserContent(sceneDirection, userMessage) {
69
+ const scene = wrapRegion(SCENE_DIRECTION_TAG, sceneDirection, true);
70
+ if (!scene) return String(userMessage ?? "");
71
+ const user = wrapRegion(USER_MESSAGE_TAG, userMessage, false);
72
+ return [scene, user].filter(Boolean).join("\n\n");
73
+ }
74
+
75
+ export function parseRoleplayUserTurn(content) {
76
+ const original = String(content ?? "");
77
+ const trimmed = original.trim();
78
+ const scene = unwrapRegion(trimmed, SCENE_DIRECTION_TAG);
79
+ if (!scene) {
80
+ return { sceneDirection: "", userMessage: original, hasMarkup: false };
81
+ }
82
+ const user = unwrapRegion(scene.rest, USER_MESSAGE_TAG);
83
+ return {
84
+ sceneDirection: unescapeXmlText(scene.inner),
85
+ userMessage: user ? user.inner : scene.rest,
86
+ hasMarkup: true
87
+ };
88
+ }
89
+
90
+ export function roleplayUserTurnDisplayText(content) {
91
+ const parsed = parseRoleplayUserTurn(content);
92
+ if (!parsed.hasMarkup) return content;
93
+ return [parsed.sceneDirection, parsed.userMessage].filter(Boolean).join("\n");
94
+ }
95
+
96
+ export function roleplayUserTurnTitleSource(content) {
97
+ const parsed = parseRoleplayUserTurn(content);
98
+ if (!parsed.hasMarkup) return content;
99
+ return parsed.userMessage || parsed.sceneDirection;
100
+ }
@@ -20,6 +20,7 @@
20
20
  --toast-fg: #fffefa;
21
21
  --toast-error-bg: #6e2e21;
22
22
  --toast-error-fg: #fffefa;
23
+ --failure-emphasis: #b54708;
23
24
  --relationship-card-border: #d9dfe8;
24
25
  --relationship-card-surface: #f4f6fa;
25
26
  --relationship-toolbar-surface: #f8fafc;
@@ -262,6 +263,7 @@
262
263
  --toast-fg: #fffefa;
263
264
  --toast-error-bg: #6e2e21;
264
265
  --toast-error-fg: #fffefa;
266
+ --failure-emphasis: #e06a28;
265
267
  --relationship-card-border: #1c1c24;
266
268
  --relationship-card-surface: #12121a;
267
269
  --relationship-toolbar-surface: #12121a;
@@ -485,8 +487,12 @@ input[type="checkbox"][hidden] { display: none; }
485
487
  .character-avatar { position: relative; display: inline-grid; flex: 0 0 auto; place-items: center; width: 36px; height: 36px; overflow: hidden; border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--line)); border-radius: 50%; background: color-mix(in srgb, var(--accent) 14%, var(--surface)); color: var(--accent-dark); font-size: 13px; font-weight: 650; line-height: 1; }
486
488
  .character-avatar img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
487
489
  .character-avatar-fallback { display: grid; width: 100%; height: 100%; place-items: center; }
488
- .account-button > .account-avatar { width: 26px; height: 26px; font-size: 11px; }
489
- .account-button > span:last-child { max-width: 100px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; }
490
+ .account-avatar-frame { position: relative; display: grid; flex: 0 0 auto; width: 26px; height: 26px; place-items: center; }
491
+ .account-button .account-avatar { width: 26px; height: 26px; font-size: 11px; }
492
+ .account-admin-mark { position: absolute; right: -3px; bottom: -2px; z-index: 1; display: grid; width: 12px; height: 12px; place-items: center; border: 1px solid var(--surface); border-radius: 50%; background: var(--accent); color: #fffefa; box-shadow: 0 1px 3px rgba(37,35,31,.22); }
493
+ .account-admin-mark svg { width: 8px; height: 8px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
494
+ .account-identity { display: flex; min-width: 0; max-width: 150px; align-items: center; gap: 5px; white-space: nowrap; font-size: 11px; }
495
+ #account-name { overflow: hidden; text-overflow: ellipsis; }
490
496
  .account-menu { position: absolute; z-index: 80; top: 48px; right: 84px; display: grid; width: 180px; min-width: 0; gap: 4px; padding: 12px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface); box-shadow: var(--shadow); }
491
497
  .account-menu.hidden { display: none; }
492
498
  .account-menu-name { display: grid; min-width: 0; gap: 2px; }
@@ -744,7 +750,7 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
744
750
  .toast.token-usage-details-toast {
745
751
  display: grid;
746
752
  grid-template-rows: auto minmax(0, 1fr);
747
- width: min(720px, calc(100vw - 48px));
753
+ width: min(1200px, calc(100vw - 48px));
748
754
  max-width: none;
749
755
  max-height: min(72dvh, 560px);
750
756
  padding: 0;
@@ -762,7 +768,7 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
762
768
  .token-usage-details-body { display: grid; gap: 10px; min-height: 0; padding: 12px 14px 14px; overflow-y: auto; color: var(--muted); font-size: 11px; line-height: 1.6; overscroll-behavior: contain; }
763
769
  .token-usage-details-note { margin: 0; }
764
770
  .token-usage-details-table-scroll { min-width: 0; overflow-x: auto; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); }
765
- .token-usage-details-table { width: 100%; min-width: 690px; border-collapse: collapse; color: var(--ink); font: 10px/1.45 var(--font-latin), var(--font-cjk), monospace; }
771
+ .token-usage-details-table { width: 100%; min-width: 820px; border-collapse: collapse; color: var(--ink); font: 10px/1.45 var(--font-latin), var(--font-cjk), monospace; }
766
772
  .token-usage-details-table caption { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
767
773
  .token-usage-details-table th, .token-usage-details-table td { padding: 9px 10px; border-bottom: 1px solid var(--line); text-align: right; white-space: nowrap; }
768
774
  .token-usage-details-table thead th { background: var(--surface-soft); color: var(--muted); font-size: 9px; font-weight: 600; letter-spacing: .03em; }
@@ -1240,11 +1246,8 @@ body.work-viewer-mode .character-editor-actions { display: none !important; }
1240
1246
  .password-settings-form { grid-template-columns: repeat(3, minmax(0, 1fr)) auto; }
1241
1247
  .api-key-settings { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 12px; margin-top: 16px; }
1242
1248
  .api-key-settings > p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.6; }
1243
- .api-key-result { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 10px; }
1244
- .api-key-result.hidden { display: none; }
1245
- .api-key-result label { display: grid; gap: 6px; color: var(--muted); font-size: 11px; }
1246
- .api-key-result input { width: 100%; min-height: 38px; padding: 9px 10px; background: var(--surface); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 11px; }
1247
- .api-key-result button { display: inline-flex; align-items: center; gap: 6px; min-height: 38px; }
1249
+ .api-key-actions { display: inline-flex; align-items: center; gap: 8px; }
1250
+ .api-key-actions button { display: inline-flex; align-items: center; gap: 6px; min-height: 38px; }
1248
1251
  .book-access-badge { display: inline-flex; width: fit-content; margin-top: 7px; padding: 2px 7px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface); color: var(--accent-dark); font-size: 9px; }
1249
1252
  .book-shelf { display: grid; grid-template-columns: repeat(auto-fill, minmax(205px, 1fr)); gap: clamp(22px, 3vw, 38px); width: 100%; min-width: 0; max-width: 1400px; margin: 0 auto; }
1250
1253
  .book-card { position: relative; min-width: 0; border: 0; background: transparent; text-align: left; }
@@ -1513,9 +1516,29 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1513
1516
  .card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 13px; }
1514
1517
  .record-card { position: relative; border: 1px solid var(--line); background: var(--surface-soft); padding: 18px; min-height: 130px; border-radius: 4px; }
1515
1518
  .record-card.has-card-edit h3 { padding-right: 34px; }
1519
+ .record-card.has-favorite-control h3 { padding-right: 70px; }
1516
1520
  .record-card-edit { position: absolute; top: 12px; right: 12px; display: inline-grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
1517
1521
  .record-card-edit:hover, .record-card-edit:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: none; }
1518
1522
  .record-card-edit svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
1523
+ .character-favorite-button { position: absolute; top: 12px; right: 48px; display: inline-grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
1524
+ .character-favorite-button:hover, .character-favorite-button:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: none; }
1525
+ .character-favorite-button.is-favorite { border-color: color-mix(in srgb, var(--accent) 58%, var(--line)); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); color: var(--accent-dark); }
1526
+ .character-favorite-button:disabled { cursor: not-allowed; opacity: .5; }
1527
+ .character-favorite-button svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.6; }
1528
+ .character-favorite-button.is-favorite svg { fill: currentColor; }
1529
+ .record-favorite-button { display: inline-grid; flex: 0 0 auto; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
1530
+ .record-favorite-button.is-card-control { position: absolute; top: 12px; right: 48px; }
1531
+ .record-favorite-button:hover, .record-favorite-button:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: none; }
1532
+ .record-favorite-button.is-favorite { border-color: color-mix(in srgb, var(--accent) 58%, var(--line)); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); color: var(--accent-dark); }
1533
+ .record-favorite-button:disabled { cursor: not-allowed; opacity: .5; }
1534
+ .record-favorite-button svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.6; }
1535
+ .record-favorite-button.is-favorite svg { fill: currentColor; }
1536
+ .entity-detail-favorite-button { display: inline-grid; flex: 0 0 36px; place-items: center; width: 36px; min-width: 36px; height: 36px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
1537
+ .entity-detail-favorite-button:hover, .entity-detail-favorite-button:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: none; }
1538
+ .entity-detail-favorite-button.is-favorite { border-color: color-mix(in srgb, var(--accent) 58%, var(--line)); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); color: var(--accent-dark); }
1539
+ .entity-detail-favorite-button:disabled { cursor: not-allowed; opacity: .5; }
1540
+ .entity-detail-favorite-button svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.6; }
1541
+ .entity-detail-favorite-button.is-favorite svg { fill: currentColor; }
1519
1542
  .record-card h3 { font-size: 16px; margin: 5px 0 10px; font-weight: 600; }
1520
1543
  .record-card p { color: var(--muted); font-size: 12px; line-height: 1.55; margin: 0; white-space: pre-wrap; }
1521
1544
  .record-card small { color: var(--accent); font-size: 9px; letter-spacing: .1em; text-transform: uppercase; }
@@ -1551,8 +1574,10 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1551
1574
  .character-card-heading { display: flex; min-width: 0; align-items: center; gap: 7px; }
1552
1575
  .character-card-heading .character-avatar { width: 32px; height: 32px; font-size: 12px; }
1553
1576
  .character-card-heading h3 { min-width: 0; }
1554
- .record-card.has-card-edit .character-card-heading { padding-right: 34px; }
1577
+ .record-card.has-card-edit .character-card-heading { padding-right: 70px; }
1555
1578
  .record-card.has-card-edit .character-card-heading h3 { padding-right: 0; }
1579
+ .module-row .character-favorite-button { position: static; }
1580
+ .module-row .record-favorite-button { position: static; }
1556
1581
  .character-lock-badge { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 3px; min-height: 20px; padding: 2px 6px; border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--line)); border-radius: 10px; background: color-mix(in srgb, var(--accent) 10%, transparent); color: var(--accent-dark); font-size: 9px; font-variant-numeric: tabular-nums; line-height: 1; }
1557
1582
  .entity-lifecycle-badge { display: inline-flex; align-items: center; min-height: 20px; margin-left: 7px; padding: 2px 7px; border: 1px solid color-mix(in srgb, var(--accent) 58%, var(--line)); border-radius: 10px; background: color-mix(in srgb, var(--accent) 13%, var(--surface)); color: var(--accent-dark); font-size: 9px; font-weight: 650; line-height: 1; vertical-align: middle; white-space: nowrap; }
1558
1583
  .character-lock-badge svg { width: 11px; height: 11px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
@@ -1597,6 +1622,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1597
1622
  .card-actions { display: flex; gap: 6px; margin-top: 15px; }
1598
1623
  .card-actions button { border: 1px solid var(--line); background: transparent; border-radius: 3px; font-size: 10px; padding: 5px 8px; }
1599
1624
  .card-actions .record-card-edit { position: static; padding: 0; }
1625
+ .card-actions .record-favorite-button { position: static; padding: 0; }
1600
1626
  .danger-button { border: 1px solid color-mix(in srgb, var(--accent) 52%, var(--line)); border-radius: 4px; background: transparent; color: var(--accent-dark); padding: 8px 13px; font-size: 12px; }
1601
1627
  .danger-button:hover, .danger-button:focus-visible { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 9%, transparent); outline: none; }
1602
1628
  .card-actions .danger-button { border-color: color-mix(in srgb, var(--accent) 52%, var(--line)); color: var(--accent-dark); }
@@ -1851,6 +1877,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1851
1877
  .task-detail ul { margin: 6px 0 0; padding-left: 18px; }
1852
1878
  .task-detail-overview { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 22px; padding: 16px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
1853
1879
  .task-detail-overview > div, .task-detail-overview > p:first-child, .task-detail-overview > p:last-child { grid-column: 1 / -1; }
1880
+ .task-detail-failures { color: var(--failure-emphasis); font-weight: 600; }
1881
+ .task-detail-failures li::marker { color: var(--failure-emphasis); }
1854
1882
  .task-detail-result {
1855
1883
  max-height: 220px;
1856
1884
  overflow: auto;
@@ -2813,6 +2841,16 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2813
2841
  .prompt-options .ai-roleplay-user-character { grid-column: 1 / -1; }
2814
2842
  .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); }
2815
2843
  .prompt-composer { position: relative; }
2844
+ .prompt-composer-leading { position: absolute; bottom: 8px; left: 8px; z-index: 2; display: flex; gap: 6px; align-items: center; }
2845
+ .ai-scene-panel { display: grid; gap: 7px; margin-bottom: 6px; padding: 8px; border: 1px solid var(--line); border-radius: 5px; background: var(--paper); }
2846
+ .ai-scene-panel.hidden { display: none; }
2847
+ .ai-scene-pin-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
2848
+ @media (max-width: 640px) {
2849
+ .ai-scene-pin-fields { grid-template-columns: minmax(0, 1fr); }
2850
+ }
2851
+ .ai-scene-panel label { display: grid; gap: 3px; color: var(--muted); font-size: 9px; }
2852
+ .ai-scene-panel input, .ai-scene-panel textarea { width: 100%; padding: 6px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 10px; }
2853
+ .ai-scene-panel textarea { min-height: 64px; resize: vertical; }
2816
2854
  .ai-image-attachments { display: flex; gap: 5px; max-height: 38px; margin-bottom: 6px; overflow-x: auto; overflow-y: hidden; padding: 5px 0 0; }
2817
2855
  .ai-image-attachment { display: inline-flex; flex: 0 0 auto; align-items: center; width: auto; height: 28px; margin: 0; padding: 2px 3px 2px 4px; border: 1px solid color-mix(in srgb, var(--accent) 48%, var(--line)); border-radius: 5px; background: var(--paper); }
2818
2856
  .ai-image-attachment-preview { display: inline-flex; align-items: center; gap: 4px; width: auto; height: 22px; min-width: 0; padding: 0 3px 0 0; border: 0; background: transparent; color: var(--ink); font: 9px/1 var(--font-cjk), sans-serif; white-space: nowrap; cursor: zoom-in; }
@@ -2828,10 +2866,11 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2828
2866
  .ai-image-preview-body { display: grid; place-items: center; min-height: 180px; max-height: calc(88vh - 106px); padding: 16px 20px 20px; overflow: auto; background: var(--paper); }
2829
2867
  .ai-image-preview-body img { display: block; max-width: 100%; max-height: calc(88vh - 150px); width: auto; height: auto; object-fit: contain; border-radius: 3px; }
2830
2868
  .ai-attachment-input { position: absolute; width: 1px; height: 1px; overflow: hidden; opacity: 0; pointer-events: none; }
2831
- .ai-attachment-button { position: absolute; bottom: 8px; left: 8px; z-index: 2; display: grid; 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); }
2832
- .ai-attachment-button:hover, .ai-attachment-button:focus-visible { 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; }
2833
- .ai-attachment-button:disabled { cursor: wait; opacity: .56; }
2834
- .ai-image-button-icon { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
2869
+ .ai-attachment-button, .ai-scene-button { display: grid; 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); }
2870
+ .ai-attachment-button:hover, .ai-attachment-button:focus-visible, .ai-scene-button:hover, .ai-scene-button:focus-visible, .ai-scene-button[aria-expanded="true"], .ai-scene-button.is-active { 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; }
2871
+ .ai-attachment-button:disabled, .ai-scene-button:disabled { cursor: wait; opacity: .56; }
2872
+ .ai-scene-button.hidden { display: none; }
2873
+ .ai-image-button-icon, .ai-scene-button-icon { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
2835
2874
  .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); }
2836
2875
  .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; }
2837
2876
  .ai-mention-option:hover, .ai-mention-option:focus-visible, .ai-mention-option.is-active { background: var(--paper-deep); }
@@ -2914,6 +2953,10 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2914
2953
  .ai-citation-card.is-expanded pre { display: block; }
2915
2954
  .message-citations { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 7px; }.message-citations span { padding: 3px 6px; border: 1px solid currentColor; border-radius: 10px; font-size: 8px; opacity: .75; }
2916
2955
  .user-message-mentions { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; min-width: 0; margin-top: 8px; padding-top: 7px; border-top: 1px solid color-mix(in srgb, currentColor 28%, transparent); font-size: 8px; line-height: 1.4; }
2956
+ .user-message-scene { margin: 0 0 8px; padding: 0 0 7px; border-bottom: 1px solid color-mix(in srgb, currentColor 28%, transparent); font-size: 10px; line-height: 1.5; }
2957
+ .user-message-scene-label { display: block; margin-bottom: 3px; opacity: .72; letter-spacing: .08em; font-size: 8px; }
2958
+ .user-message-scene-body { white-space: pre-wrap; overflow-wrap: anywhere; }
2959
+ .user-message .message-body:empty { display: none; }
2917
2960
  .user-message-mentions-label { flex: 0 0 auto; opacity: .72; letter-spacing: .08em; }
2918
2961
  .user-message-mention { min-width: 0; max-width: 100%; padding: 2px 6px; overflow-wrap: anywhere; border: 1px solid color-mix(in srgb, currentColor 55%, transparent); border-radius: 999px; background: color-mix(in srgb, currentColor 12%, transparent); }
2919
2962
 
@@ -2948,7 +2991,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2948
2991
  right: auto;
2949
2992
  bottom: auto;
2950
2993
  left: 50%;
2951
- width: min(720px, calc(100vw - 32px));
2994
+ width: min(1200px, calc(100vw - 32px));
2952
2995
  max-width: calc(100vw - 32px);
2953
2996
  transform: translate(-50%, -50%);
2954
2997
  }
@@ -3075,6 +3118,13 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3075
3118
  .dialog::backdrop { background: rgba(34,30,25,.42); backdrop-filter: blur(3px); }
3076
3119
  .dialog-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 22px 24px 16px; border-bottom: 1px solid var(--line); }
3077
3120
  .dialog-header h2 { margin: 0; font-size: 23px; font-weight: 500; }
3121
+ #form-dialog .dialog-header { gap: 12px; }
3122
+ #form-dialog .dialog-header > div:first-child { flex: 1 1 auto; min-width: 0; }
3123
+ #form-dialog .settings-dialog-header-actions { flex: 0 0 auto; }
3124
+ #form-dialog .dialog-title-input { width: min(560px, 100%); }
3125
+ .dialog-context-actions { display: flex; align-items: center; gap: 8px; }
3126
+ .dialog-context-actions:empty { display: none; }
3127
+ .dialog-context-actions .ghost-button { min-height: 36px; white-space: nowrap; }
3078
3128
  .dialog-title-input { display: block; width: min(560px, calc(100vw - 150px)); min-width: 0; margin: -5px 0 0; padding: 0 0 4px; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; background: transparent; color: var(--ink); font-size: 23px; font-weight: 500; line-height: 1.3; }
3079
3129
  .dialog-title-input:focus { border-bottom-color: var(--accent); outline: none; }
3080
3130
  .dialog-title-input::placeholder { color: var(--muted); opacity: .8; }
@@ -3521,7 +3571,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3521
3571
  .character-editor-section > header p { max-width: 430px; margin: 0; color: var(--muted); font-size: 10px; line-height: 1.55; text-align: right; }
3522
3572
  .character-editor-section-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 17px 20px; }
3523
3573
  .character-editor-section-fields > label, .character-editor-section-fields > .form-field { display: grid; align-content: start; gap: 7px; color: var(--muted); font-size: 11px; }
3524
- .character-editor-section-fields > label:has(textarea), .character-editor-section-fields > .structured-list-field, .character-editor-section-fields > .chip-field, .character-editor-section-fields > .markdown-editor-field, .character-editor-field-help { grid-column: 1 / -1; }
3574
+ .character-editor-section-fields > label:has(textarea), .character-editor-section-fields > .form-field:has(textarea), .character-editor-section-fields > .structured-list-field, .character-editor-section-fields > .chip-field, .character-editor-section-fields > .markdown-editor-field, .character-editor-field-help { grid-column: 1 / -1; }
3525
3575
  .character-editor-section-fields input, .character-editor-section-fields textarea, .character-editor-section-fields select { width: 100%; padding: 10px 11px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); font-size: 13px; }
3526
3576
  .character-editor-section-fields textarea { min-height: 120px; resize: vertical; line-height: 1.65; }
3527
3577
  .character-editor-section-fields .section-list-row textarea { min-height: 180px; }
@@ -3802,7 +3852,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3802
3852
  .ai-history-dialog-body { min-height: min(360px, 62vh); padding: 12px 14px 18px; }
3803
3853
  .account-settings-body { padding: 16px; }
3804
3854
  .account-settings-section { padding: 15px; }
3805
- .account-settings-form, .password-settings-form, .api-key-settings, .api-key-result { grid-template-columns: minmax(0, 1fr); }
3855
+ .account-settings-form, .password-settings-form, .api-key-settings { grid-template-columns: minmax(0, 1fr); }
3856
+ .api-key-actions { width: 100%; }
3857
+ .api-key-actions button { flex: 1; }
3806
3858
  .avatar-settings { grid-template-columns: 64px minmax(0, 1fr); }
3807
3859
  .profile-avatar-preview { width: 64px; height: 64px; }
3808
3860
  .character-avatar-editor-preview { width: 64px; height: 64px; }
@@ -3813,6 +3865,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3813
3865
  .account-settings-form .primary-button { width: 100%; }
3814
3866
  .usage-stat-grid { grid-template-columns: minmax(0, 1fr); }
3815
3867
  .usage-stat { min-height: 104px; }
3868
+ .ai-scene-pin-fields { grid-template-columns: minmax(0, 1fr); }
3816
3869
  }
3817
3870
 
3818
3871
  /* 移动端工作台:仅在窄屏覆盖桌面三栏布局,保留桌面端原有尺寸与排列。 */
@@ -4099,6 +4152,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
4099
4152
  .entity-editor-header, .character-editor-header { grid-template-columns: auto minmax(0, 1fr); gap: 8px; padding: 12px var(--mobile-gutter); }
4100
4153
  .entity-editor-mode-actions, .character-editor-header-actions { grid-column: 1 / -1; justify-content: stretch; flex-wrap: wrap; }
4101
4154
  .entity-editor-mode-actions > button, .character-editor-header-actions > button { flex: 1 1 auto; }
4155
+ .entity-editor-mode-actions > .entity-detail-favorite-button, .character-editor-header-actions > .entity-detail-favorite-button { flex: 0 0 40px; width: 40px; min-width: 40px; height: 40px; }
4156
+ .dialog-context-actions .entity-detail-favorite-button { flex-basis: 40px; width: 40px; min-width: 40px; height: 40px; }
4102
4157
  .setting-editor-header { min-height: 0; }
4103
4158
  .setting-editor-content { padding: 12px; }
4104
4159
  .setting-markdown-field { height: 68dvh; }
@@ -4176,6 +4231,10 @@ body { font-size: var(--ui-font-size); }
4176
4231
  .prompt-options select, .ai-model-popover > label, .ai-model-option-copy strong, .ai-model-options-empty, .ai-mention-option strong, .ai-mention-empty { font-size: calc(10px * var(--ai-font-scale)); }
4177
4232
  .ai-mention-option small { font-size: calc(8px * var(--ai-font-scale)); }
4178
4233
  .ai-prompt { font-size: var(--ai-font-size); }
4234
+ .ai-scene-panel label { font-size: calc(9px * var(--ai-font-scale)); }
4235
+ .ai-scene-panel input, .ai-scene-panel textarea { font-size: calc(10px * var(--ai-font-scale)); }
4236
+ .user-message-scene { font-size: calc(10px * var(--ai-font-scale)); }
4237
+ .user-message-scene-label { font-size: calc(8px * var(--ai-font-scale)); }
4179
4238
  .ai-prompt-reference { font-size: calc(10px * var(--ai-font-scale)); }
4180
4239
  .ai-context-meter b { font-size: calc(9px * var(--ai-font-scale)); }
4181
4240
  .ai-context-popover-header strong { font-size: calc(12px * var(--ai-font-scale)); }
@@ -0,0 +1,4 @@
1
+ export function resolveToastRegionHost<TDialog extends { open?: boolean }, TFallback>(
2
+ dialogs: ReadonlyArray<TDialog | null | undefined> | null | undefined,
3
+ fallbackHost?: TFallback | null
4
+ ): TDialog | TFallback | null;
@@ -0,0 +1,5 @@
1
+ /** 打开的模态对话框会让文档其余部分 inert;Toast 必须挂到最顶层对话框内才能接收点击。 */
2
+ export function resolveToastRegionHost(dialogs, fallbackHost) {
3
+ const openDialogs = (Array.isArray(dialogs) ? dialogs : []).filter((dialog) => Boolean(dialog?.open));
4
+ return openDialogs.at(-1) ?? fallbackHost ?? null;
5
+ }
@@ -0,0 +1,98 @@
1
+ const SCENE_DIRECTION_TAG = "scene_direction";
2
+ const USER_MESSAGE_TAG = "user_message";
3
+ const SCENE_PIN_LOCATION_MAX = 200;
4
+ const SCENE_PIN_PRESENT_MAX = 500;
5
+ const SCENE_PIN_TIME_MAX = 200;
6
+ function escapeXmlText(text) {
7
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;");
8
+ }
9
+ function unescapeXmlText(text) {
10
+ return text.replaceAll("&lt;", "<").replaceAll("&amp;", "&");
11
+ }
12
+ function wrapRegion(tag, body, escape = true) {
13
+ const trimmed = body.trim();
14
+ if (!trimmed)
15
+ return "";
16
+ const content = escape ? escapeXmlText(trimmed) : trimmed;
17
+ return `<${tag}>\n${content}\n</${tag}>`;
18
+ }
19
+ function unwrapRegion(content, tag) {
20
+ const open = `<${tag}>`;
21
+ const close = `</${tag}>`;
22
+ if (!content.startsWith(open))
23
+ return null;
24
+ const closeIndex = content.indexOf(close, open.length);
25
+ if (closeIndex < 0)
26
+ return null;
27
+ const inner = content.slice(open.length, closeIndex).replace(/^\n/u, "").replace(/\n$/u, "");
28
+ const rest = content.slice(closeIndex + close.length).trim();
29
+ return { inner, rest };
30
+ }
31
+ export function emptyRoleplayScenePin() {
32
+ return { location: "", present: "", timeLabel: "" };
33
+ }
34
+ export function normalizeRoleplayScenePin(value) {
35
+ const source = value && typeof value === "object" && !Array.isArray(value)
36
+ ? value
37
+ : {};
38
+ const clip = (raw, maximum) => (typeof raw === "string" ? raw.trim().slice(0, maximum) : "");
39
+ return {
40
+ location: clip(source.location, SCENE_PIN_LOCATION_MAX),
41
+ present: clip(source.present, SCENE_PIN_PRESENT_MAX),
42
+ timeLabel: clip(source.timeLabel, SCENE_PIN_TIME_MAX)
43
+ };
44
+ }
45
+ export function roleplayScenePinHasContent(pin) {
46
+ return Boolean(pin.location || pin.present || pin.timeLabel);
47
+ }
48
+ export function formatRoleplayScenePinText(pin) {
49
+ const normalized = normalizeRoleplayScenePin(pin);
50
+ const lines = [];
51
+ if (normalized.location)
52
+ lines.push(`地点:${normalized.location}`);
53
+ if (normalized.present)
54
+ lines.push(`在场:${normalized.present}`);
55
+ if (normalized.timeLabel)
56
+ lines.push(`故事时间:${normalized.timeLabel}`);
57
+ return lines.join("\n");
58
+ }
59
+ export function composeRoleplayCurrentUserTurn(sceneDirection, userMessage) {
60
+ return [
61
+ wrapRegion(SCENE_DIRECTION_TAG, sceneDirection, true),
62
+ wrapRegion(USER_MESSAGE_TAG, userMessage, false)
63
+ ].filter(Boolean).join("\n\n");
64
+ }
65
+ export function composeRoleplayStoredUserContent(sceneDirection, userMessage) {
66
+ const scene = wrapRegion(SCENE_DIRECTION_TAG, sceneDirection, true);
67
+ if (!scene)
68
+ return userMessage;
69
+ const user = wrapRegion(USER_MESSAGE_TAG, userMessage, false);
70
+ return [scene, user].filter(Boolean).join("\n\n");
71
+ }
72
+ export function parseRoleplayUserTurn(content) {
73
+ const original = String(content ?? "");
74
+ const trimmed = original.trim();
75
+ const scene = unwrapRegion(trimmed, SCENE_DIRECTION_TAG);
76
+ if (!scene) {
77
+ return { sceneDirection: "", userMessage: original, hasMarkup: false };
78
+ }
79
+ const user = unwrapRegion(scene.rest, USER_MESSAGE_TAG);
80
+ return {
81
+ sceneDirection: unescapeXmlText(scene.inner),
82
+ userMessage: user ? user.inner : scene.rest,
83
+ hasMarkup: true
84
+ };
85
+ }
86
+ export function roleplayUserTurnDisplayText(content) {
87
+ const parsed = parseRoleplayUserTurn(content);
88
+ if (!parsed.hasMarkup)
89
+ return content;
90
+ return [parsed.sceneDirection, parsed.userMessage].filter(Boolean).join("\n");
91
+ }
92
+ export function roleplayUserTurnTitleSource(content) {
93
+ const parsed = parseRoleplayUserTurn(content);
94
+ if (!parsed.hasMarkup)
95
+ return content;
96
+ return parsed.userMessage || parsed.sceneDirection;
97
+ }
98
+ //# sourceMappingURL=roleplay-turn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roleplay-turn.js","sourceRoot":"","sources":["../src/roleplay-turn.ts"],"names":[],"mappings":"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"}