@musnows/scriverse 0.8.0 → 0.8.2
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/ai-conversation-export.js +3 -1
- package/dist/ai-conversation-export.js.map +1 -1
- package/dist/ai.js +72 -6
- package/dist/ai.js.map +1 -1
- package/dist/app.js +48 -14
- package/dist/app.js.map +1 -1
- package/dist/database.js +26 -2
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +326 -72
- package/dist/public/index.html +12 -7
- package/dist/public/relationship-graph.js +11 -3
- package/dist/public/stream-typewriter.d.ts +1 -0
- package/dist/public/stream-typewriter.js +12 -0
- package/dist/public/styles.css +33 -17
- package/dist/public/work-permissions.d.ts +1 -1
- package/dist/public/work-permissions.js +13 -1
- package/dist/store.js +90 -16
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +34 -7
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/work-permissions.js +13 -2
- package/dist/work-permissions.js.map +1 -1
- package/package.json +1 -1
package/dist/public/index.html
CHANGED
|
@@ -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">
|
|
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">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -133,8 +133,8 @@
|
|
|
133
133
|
<button type="button" data-module="relationships"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.59 13.51 6.83 3.98"/><path d="m15.41 6.51-6.82 3.98"/></svg>关系</button>
|
|
134
134
|
<button type="button" data-module="races"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="4" r="2"/><circle cx="18" cy="8" r="2"/><circle cx="20" cy="16" r="2"/><path d="M9 10a5 5 0 0 1 5 5v3.5a3.5 3.5 0 0 1-6.84 1.045Q6.52 17.48 4.46 16.84A3.5 3.5 0 0 1 5.5 10Z"/></svg>种族</button>
|
|
135
135
|
<button class="ai-analysis-entry" type="button" data-module="tasks"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>AI 分析</button>
|
|
136
|
-
<button id="ai-assistant-entry" class="ai-assistant-entry" type="button" aria-controls="ai-panel" aria-expanded="false"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3h11A2.5 2.5 0 0 1 20 5.5v8a2.5 2.5 0 0 1-2.5 2.5H11l-4.5 4v-4h0A2.5 2.5 0 0 1 4 13.5z"/><path d="M8 8h8M8 11h5"/></svg>创作助手</button>
|
|
137
136
|
<button id="module-more-button" type="button" aria-expanded="false"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span class="nav-label">更多</span></button>
|
|
137
|
+
<button id="ai-assistant-entry" class="module-nav-secondary hidden ai-assistant-entry" type="button" aria-controls="ai-panel" aria-expanded="false"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3h11A2.5 2.5 0 0 1 20 5.5v8a2.5 2.5 0 0 1-2.5 2.5H11l-4.5 4v-4h0A2.5 2.5 0 0 1 4 13.5z"/><path d="M8 8h8M8 11h5"/></svg>创作助手</button>
|
|
138
138
|
<button id="reader-open-button" class="module-nav-secondary hidden" type="button" aria-label="打开沉浸式阅读预览" title="阅读预览"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 5.5A2.5 2.5 0 0 1 5.5 3H11v16H5.5A2.5 2.5 0 0 0 3 21.5Z"/><path d="M21 5.5A2.5 2.5 0 0 0 18.5 3H13v16h5.5a2.5 2.5 0 0 1 2.5 2.5Z"/></svg>阅读预览</button>
|
|
139
139
|
<button class="module-nav-secondary hidden" type="button" data-module="comments"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4Z"/><path d="M8 9h8M8 13h5"/></svg><span class="nav-label">正文评论</span></button>
|
|
140
140
|
<button class="module-nav-secondary hidden" type="button" data-module="outlines"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></svg><span class="nav-label">大纲/伏笔</span></button>
|
|
@@ -379,9 +379,9 @@
|
|
|
379
379
|
<select id="ai-roleplay-character" class="ai-roleplay-character hidden" aria-label="角色扮演角色卡">
|
|
380
380
|
<option value="">选择角色卡</option>
|
|
381
381
|
</select>
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
382
|
+
<select id="ai-roleplay-user-character" class="ai-roleplay-character ai-roleplay-user-character hidden" aria-label="我扮演的角色">
|
|
383
|
+
<option value="">选择我的角色(可选)</option>
|
|
384
|
+
</select>
|
|
385
385
|
</div>
|
|
386
386
|
<div class="prompt-composer">
|
|
387
387
|
<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,6 +392,11 @@
|
|
|
392
392
|
<header class="ai-context-popover-header"><div><strong id="ai-context-popover-title">Token 分布</strong><small id="ai-context-popover-description">按当前模型上下文窗口计算</small></div><button id="ai-context-popover-close" class="ai-context-popover-close" type="button" aria-label="关闭 Token 分布">×</button></header>
|
|
393
393
|
<div id="ai-context-distribution" class="ai-context-distribution" role="list" aria-label="当前 Token 分布"></div>
|
|
394
394
|
</section>
|
|
395
|
+
<button id="ai-model-picker" class="ai-model-picker" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="ai-model-popover" aria-label="选择实际使用模型" title="选择实际使用模型"><svg class="ai-model-picker-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 5.25a3.25 3.25 0 0 0-5.95 1.82A3.74 3.74 0 0 0 4.7 13.5a3.25 3.25 0 0 0 4.55 4.53A3.6 3.6 0 0 0 12 19.5"></path><path d="M12 5.25a3.25 3.25 0 0 1 5.95 1.82 3.74 3.74 0 0 1 1.35 6.43 3.25 3.25 0 0 1-4.55 4.53A3.6 3.6 0 0 1 12 19.5"></path><path d="M12 5.25v14.25"></path><path d="M8 9.1c1.1.1 2 .9 2.25 1.95"></path><path d="M16 9.1c-1.1.1-2 .9-2.25 1.95"></path><path d="M8.3 15.3c1.15-.05 2.1-.85 2.3-1.95"></path><path d="M15.7 15.3c-1.15-.05-2.1-.85-2.3-1.95"></path></svg></button>
|
|
396
|
+
<section id="ai-model-popover" class="ai-model-popover hidden" role="dialog" aria-labelledby="ai-model-popover-title">
|
|
397
|
+
<label id="ai-model-popover-title" for="ai-model">实际使用模型</label>
|
|
398
|
+
<select id="ai-model" aria-label="实际使用模型"><option value="">使用创作助手时加载模型</option></select>
|
|
399
|
+
</section>
|
|
395
400
|
<button id="ai-send" class="ai-send-button" type="button" data-state="send" aria-label="发送消息" title="发送消息"><svg class="ai-send-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M21 3 9.5 14.5M21 3l-7 18-4.5-6.5L3 10l18-7Z"></path></svg></button>
|
|
396
401
|
</div>
|
|
397
402
|
</div>
|
|
@@ -1106,7 +1111,7 @@
|
|
|
1106
1111
|
<button type="button" data-permission-preset="write">全部可编辑</button>
|
|
1107
1112
|
</div>
|
|
1108
1113
|
<div id="member-permission-grid" class="member-permission-grid"></div>
|
|
1109
|
-
<small class="member-permission-help"
|
|
1114
|
+
<small class="member-permission-help">正文评论和正文待办的“可添加”只控制新增;删除、内容更新和状态更新需要正文可编辑权限。未授权模块不会显示,也不能通过 API 读取。</small>
|
|
1110
1115
|
<button id="member-permission-submit" class="primary-button" type="submit">保存成员权限</button>
|
|
1111
1116
|
</fieldset>
|
|
1112
1117
|
</form>
|
|
@@ -1205,6 +1210,6 @@
|
|
|
1205
1210
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
1206
1211
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
1207
1212
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
1208
|
-
<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=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-v1&feature=volume-detail-icon-v1&feature=reader-manual-chapter-navigation-v1"></script>
|
|
1213
|
+
<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-v1&feature=volume-detail-icon-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-model-picker-v1"></script>
|
|
1209
1214
|
</body>
|
|
1210
1215
|
</html>
|
|
@@ -327,10 +327,18 @@ export function normalizeGalaxyMotionMode(value) {
|
|
|
327
327
|
return GALAXY_MOTION_MODES.includes(value) ? value : "auto";
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
export function isGalaxyPerformanceThresholdExceeded(nodeCount, edgeCount) {
|
|
331
|
+
return Math.max(0, Number(nodeCount) || 0) > GALAXY_REDUCED_MOTION_NODE_THRESHOLD
|
|
332
|
+
|| Math.max(0, Number(edgeCount) || 0) > GALAXY_REDUCED_MOTION_EDGE_THRESHOLD;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function shouldRenderGalaxyEdgeLabel(nodeCount, edgeCount, edgeSelected, highlighted) {
|
|
336
|
+
return Boolean(edgeSelected) || (Boolean(highlighted) && !isGalaxyPerformanceThresholdExceeded(nodeCount, edgeCount));
|
|
337
|
+
}
|
|
338
|
+
|
|
330
339
|
export function getGalaxyMotionProfile(mode, nodeCount, edgeCount, prefersReducedMotion = false) {
|
|
331
340
|
const requestedMode = normalizeGalaxyMotionMode(mode);
|
|
332
|
-
const thresholdExceeded =
|
|
333
|
-
|| Math.max(0, Number(edgeCount) || 0) > GALAXY_REDUCED_MOTION_EDGE_THRESHOLD;
|
|
341
|
+
const thresholdExceeded = isGalaxyPerformanceThresholdExceeded(nodeCount, edgeCount);
|
|
334
342
|
const reducedBySystem = requestedMode === "auto" && Boolean(prefersReducedMotion);
|
|
335
343
|
const reducedByThreshold = requestedMode === "auto" && thresholdExceeded;
|
|
336
344
|
const effectiveMode = requestedMode === "off"
|
|
@@ -2528,7 +2536,7 @@ export function createGalaxyRenderer(dialog, graph, options = {}) {
|
|
|
2528
2536
|
context.lineTo(to.x - Math.cos(angle + 0.45) * arrowSize, to.y - Math.sin(angle + 0.45) * arrowSize);
|
|
2529
2537
|
context.fill();
|
|
2530
2538
|
}
|
|
2531
|
-
if (highlighted) {
|
|
2539
|
+
if (shouldRenderGalaxyEdgeLabel(graph.stats.nodeCount, graph.stats.edgeCount, edgeSelected, highlighted)) {
|
|
2532
2540
|
const fullLabel = formatRelationshipLabel(edge);
|
|
2533
2541
|
if (!highlightedKeywordSet.has(fullLabel)) {
|
|
2534
2542
|
highlightedKeywordSet.add(fullLabel);
|
|
@@ -122,6 +122,18 @@ export function createStreamTypewriter({
|
|
|
122
122
|
pendingCharacters.push(...characters);
|
|
123
123
|
schedule();
|
|
124
124
|
},
|
|
125
|
+
replace(value) {
|
|
126
|
+
if (scheduledFrame !== null) {
|
|
127
|
+
cancelFrame(scheduledFrame);
|
|
128
|
+
scheduledFrame = null;
|
|
129
|
+
}
|
|
130
|
+
visibleCharacters.splice(0, visibleCharacters.length, ...Array.from(String(value ?? "")));
|
|
131
|
+
pendingCharacters.splice(0);
|
|
132
|
+
finishing = false;
|
|
133
|
+
render();
|
|
134
|
+
resolveIdle();
|
|
135
|
+
return snapshot();
|
|
136
|
+
},
|
|
125
137
|
finish() {
|
|
126
138
|
if (!pendingCharacters.length && scheduledFrame === null) return Promise.resolve(snapshot());
|
|
127
139
|
finishing = true;
|
package/dist/public/styles.css
CHANGED
|
@@ -371,10 +371,7 @@ input[type="checkbox"][hidden] { display: none; }
|
|
|
371
371
|
radial-gradient(circle at 50% -20%, var(--app-glow), transparent 45%),
|
|
372
372
|
var(--paper);
|
|
373
373
|
}
|
|
374
|
-
.app-shell:not(.shelf-mode):not(.ai-workspace-mode) { grid-template-columns: var(--left-panel-width) minmax(480px, 1fr); }
|
|
375
|
-
.app-shell:not(.shelf-mode):not(.ai-workspace-mode) .ai-panel { display: none; }
|
|
376
374
|
.app-shell.ai-workspace-mode > :is(.topbar, .left-panel, .main-panel) { visibility: hidden; pointer-events: none; }
|
|
377
|
-
|
|
378
375
|
.topbar {
|
|
379
376
|
grid-column: 1 / -1;
|
|
380
377
|
display: grid;
|
|
@@ -557,7 +554,7 @@ input[type="checkbox"][hidden] { display: none; }
|
|
|
557
554
|
.left-panel-body { flex: 1 1 auto; min-height: 0; padding: 18px 14px 16px; overflow-y: auto; }
|
|
558
555
|
.ai-panel { container-type: inline-size; border-left: 1px solid var(--line); display: flex; flex-direction: column; padding: 14px 16px 18px; min-width: 0; }
|
|
559
556
|
.panel-collapse-button { z-index: 8; flex: 0 0 auto; width: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: var(--panel); color: var(--muted); font-size: 18px; line-height: 1; }
|
|
560
|
-
.ai-heading #ai-panel-toggle { flex-basis: 30px; width: 30px; height: 30px; }
|
|
557
|
+
.ai-heading #ai-panel-toggle { flex-basis: 30px; width: 30px; min-width: 30px; min-height: 30px; height: 30px; }
|
|
561
558
|
.panel-resize-handle { position: absolute; z-index: 7; top: 0; bottom: 0; width: 7px; cursor: col-resize; touch-action: none; }
|
|
562
559
|
.left-panel > .panel-resize-handle { right: 0; }.ai-panel > .panel-resize-handle { left: 0; }
|
|
563
560
|
.panel-resize-handle::after { content: ""; position: absolute; top: 0; bottom: 0; left: 3px; width: 1px; background: transparent; transition: background .14s ease; }
|
|
@@ -1368,7 +1365,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1368
1365
|
|
|
1369
1366
|
.hidden { display: none !important; }
|
|
1370
1367
|
.editor-view { container-name: editor-workspace; container-type: inline-size; display: grid; grid-template-rows: auto auto minmax(0, 1fr); height: 100%; }
|
|
1371
|
-
.editor-toolbar { position: relative; display: grid; grid-template-areas: "path path" "title actions"; grid-template-columns: minmax(0, 1fr) auto; align-items: flex-end; column-gap: 20px; row-gap: 6px; padding: 25px 7% 18px; border-bottom: 1px solid var(--line); }
|
|
1368
|
+
.editor-toolbar { position: relative; display: grid; grid-row: 1; grid-template-areas: "path path" "title actions"; grid-template-columns: minmax(0, 1fr) auto; align-items: flex-end; column-gap: 20px; row-gap: 6px; padding: 25px 7% 18px; border-bottom: 1px solid var(--line); }
|
|
1372
1369
|
#chapter-path { grid-area: path; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1373
1370
|
.chapter-title { grid-area: title; display: block; width: 100%; min-width: 0; border: 0; border-radius: 0; background: transparent; font-size: 27px; padding: 0; }
|
|
1374
1371
|
.chapter-title:focus { box-shadow: none; border-bottom: 1px solid var(--accent); }
|
|
@@ -1388,7 +1385,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1388
1385
|
.chapter-search-actions > span { flex: 1 1 120px; min-width: 0; color: var(--muted); font-size: 10px; line-height: 1.4; }
|
|
1389
1386
|
.chapter-search-actions button { min-height: 32px; padding: 6px 9px; font-size: 10px; }
|
|
1390
1387
|
.chapter-search-actions button:disabled { cursor: not-allowed; opacity: .45; }
|
|
1391
|
-
.chapter-foreshadow-reminder { --reminder-color: #80713d; display: grid; grid-template-areas: "summary actions" "details details"; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 9px 18px; padding: 10px 7%; border-bottom: 1px solid color-mix(in srgb, var(--reminder-color) 32%, var(--line)); background: color-mix(in srgb, var(--reminder-color) 7%, var(--surface)); }
|
|
1388
|
+
.chapter-foreshadow-reminder { --reminder-color: #80713d; display: grid; grid-row: 2; grid-template-areas: "summary actions" "details details"; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 9px 18px; padding: 10px 7%; border-bottom: 1px solid color-mix(in srgb, var(--reminder-color) 32%, var(--line)); background: color-mix(in srgb, var(--reminder-color) 7%, var(--surface)); }
|
|
1392
1389
|
.chapter-foreshadow-reminder[data-role="payoff"] { --reminder-color: #49745c; }
|
|
1393
1390
|
.chapter-foreshadow-reminder-summary { grid-area: summary; display: flex; min-width: 0; align-items: center; gap: 10px; }
|
|
1394
1391
|
.chapter-foreshadow-reminder-summary > div { display: grid; min-width: 0; gap: 2px; }
|
|
@@ -1411,11 +1408,16 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1411
1408
|
.chapter-foreshadow-reminder-actions { justify-content: flex-start; flex-wrap: wrap; }
|
|
1412
1409
|
.chapter-foreshadow-reminder-details { grid-template-columns: minmax(0, 1fr); }
|
|
1413
1410
|
}
|
|
1414
|
-
.editor-body { display: flex; min-height: 0; flex-direction: column; }
|
|
1411
|
+
.editor-body { display: flex; grid-row: 3; min-height: 0; flex-direction: column; }
|
|
1415
1412
|
.chapter-editor-frame { position: relative; display: grid; grid-template-columns: 38px minmax(0, 1fr); flex: 1 1 auto; width: 100%; min-height: 0; margin: 0; overflow: hidden; }
|
|
1416
1413
|
.chapter-line-numbers { position: relative; z-index: 2; grid-column: 1; grid-row: 1; min-height: 0; overflow: visible; border-right: 1px solid var(--line); background: linear-gradient(to right, transparent, rgba(143,132,116,.04)); color: var(--muted); font-family: var(--font-latin), monospace; font-size: clamp(11px, calc(var(--editor-font-size) * .72), 14px); line-height: 1.2; user-select: none; }
|
|
1417
1414
|
#chapter-line-numbers-inner { position: absolute; top: 32px; left: 0; right: 0; will-change: transform; }
|
|
1418
|
-
.chapter-line-number { position: absolute; left: 0; box-sizing: border-box; display: flex; width: 100%; align-items: flex-start; justify-content: center; padding: 0
|
|
1415
|
+
.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
|
+
.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
|
+
.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); }
|
|
1419
|
+
.chapter-line-annotation-count:hover, .chapter-line-annotation-count:focus-visible { background: var(--accent-dark); }
|
|
1420
|
+
.chapter-line-annotation-count:focus-visible { outline: 1px solid var(--accent-dark); outline-offset: 1px; }
|
|
1419
1421
|
.chapter-line-number:hover { background: rgba(139,61,44,.08); color: var(--accent-dark); }
|
|
1420
1422
|
.chapter-line-number.is-line-selected { background: rgba(139,61,44,.16); color: var(--accent-dark); font-weight: 700; }
|
|
1421
1423
|
.chapter-line-number.is-line-selected::after { content: ""; position: absolute; top: 0; left: 100%; width: 100vw; height: 100%; background: rgba(139,61,44,.085); box-shadow: inset 3px 0 rgba(139,61,44,.28); pointer-events: none; }
|
|
@@ -2619,6 +2621,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2619
2621
|
.ai-error-settings-link:hover, .ai-error-settings-link:focus-visible { color: var(--accent-dark); }
|
|
2620
2622
|
.config-section.is-targeted { border-radius: 5px; box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 34%, transparent); }
|
|
2621
2623
|
.message-card-actions { position: absolute; right: 0; bottom: -31px; display: flex; align-items: center; gap: 12px; height: 24px; }
|
|
2624
|
+
.assistant-message .message-card-actions { right: auto; left: 0; }
|
|
2625
|
+
.user-message .message-card-actions { right: 0; left: auto; }
|
|
2622
2626
|
.message-card-actions button { display: inline-flex; align-items: center; gap: 4px; min-width: 0; padding: 3px 2px; border: 0; background: transparent; color: var(--muted); font-size: 9px; }
|
|
2623
2627
|
.message-card-actions button:hover, .message-card-actions button:focus-visible { color: var(--accent-dark); }
|
|
2624
2628
|
.message-action-icon { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
|
|
@@ -2707,9 +2711,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2707
2711
|
.prompt-options { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 7px; }
|
|
2708
2712
|
.prompt-options select { padding: 6px; color: var(--muted); font-size: 10px; }
|
|
2709
2713
|
.prompt-options .ai-roleplay-character { min-width: 0; }
|
|
2714
|
+
.prompt-options .ai-roleplay-user-character { grid-column: 1 / -1; }
|
|
2710
2715
|
.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); }
|
|
2711
|
-
.prompt-model-field { margin: 0 0 7px; }
|
|
2712
|
-
.prompt-model-field select { padding: 6px; color: var(--muted); font-size: 10px; }
|
|
2713
2716
|
.prompt-composer { position: relative; }
|
|
2714
2717
|
.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); }
|
|
2715
2718
|
.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; }
|
|
@@ -2725,20 +2728,31 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2725
2728
|
.ai-prompt-reference button { width: 16px; height: 16px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: inherit; font: 14px/1 var(--font-latin), monospace; }
|
|
2726
2729
|
.ai-prompt-reference button:hover, .ai-prompt-reference button:focus-visible { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }
|
|
2727
2730
|
.prompt-composer-actions { position: absolute; right: 8px; bottom: 8px; display: flex; gap: 6px; align-items: center; }
|
|
2728
|
-
.ai-context-meter { --context-usage: 0; --context-meter-color: var(--green); position: relative; display: grid; flex: 0 0 32px; place-items: center; width: 32px; height: 32px; overflow: visible; border: 0; border-radius: 50%; background: conic-gradient(var(--context-meter-color) calc(var(--context-usage) * 1%), rgba(143,132,116,.2) 0); color: var(--ink); font-family: var(--font-latin); line-height: 1; cursor: pointer; }
|
|
2731
|
+
.ai-context-meter { --context-usage: 0; --context-meter-color: var(--green); position: relative; display: grid; flex: 0 0 32px; place-items: center; width: 32px; min-height: 32px; height: 32px; overflow: visible; border: 0; border-radius: 50%; background: conic-gradient(var(--context-meter-color) calc(var(--context-usage) * 1%), rgba(143,132,116,.2) 0); color: var(--ink); font-family: var(--font-latin); line-height: 1; cursor: pointer; }
|
|
2729
2732
|
.ai-context-meter::before { content: ""; position: absolute; inset: 3px; border-radius: 50%; background: var(--paper); box-shadow: inset 0 0 0 1px rgba(143,132,116,.12); }
|
|
2730
2733
|
.ai-context-meter:hover, .ai-context-meter:focus-visible, .ai-context-meter[aria-expanded="true"] { outline: 0; filter: brightness(.96); }
|
|
2731
2734
|
.ai-context-meter:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
|
|
2732
2735
|
.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; }
|
|
2733
2736
|
.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); }
|
|
2734
2737
|
.ai-context-popover.hidden { display: none; }
|
|
2735
|
-
.ai-context-popover::after { position: absolute; right:
|
|
2738
|
+
.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); }
|
|
2736
2739
|
.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); }
|
|
2737
2740
|
.ai-context-popover-header > div { display: grid; gap: 3px; min-width: 0; }
|
|
2738
2741
|
.ai-context-popover-header strong { font-size: 12px; }
|
|
2739
2742
|
.ai-context-popover-header small { color: var(--muted); font: 9px/1.4 var(--font-latin), var(--font-cjk), sans-serif; }
|
|
2740
2743
|
.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; }
|
|
2741
2744
|
.ai-context-popover-close:hover, .ai-context-popover-close:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: 0; }
|
|
2745
|
+
.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); }
|
|
2746
|
+
.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; }
|
|
2747
|
+
.ai-model-picker:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); }
|
|
2748
|
+
.ai-model-picker:disabled { cursor: wait; opacity: .56; }
|
|
2749
|
+
.ai-model-picker-icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
|
|
2750
|
+
.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); }
|
|
2751
|
+
.ai-model-popover.hidden { display: none; }
|
|
2752
|
+
.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); }
|
|
2753
|
+
.ai-model-popover > label { color: var(--muted); font-size: 10px; font-weight: 600; }
|
|
2754
|
+
.ai-model-popover select { width: 100%; min-width: 0; padding: 7px; color: var(--ink); font-size: 10px; }
|
|
2755
|
+
.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); }
|
|
2742
2756
|
.ai-context-distribution { display: grid; gap: 10px; }
|
|
2743
2757
|
.ai-context-distribution-row { display: grid; gap: 5px; }
|
|
2744
2758
|
.ai-context-distribution-label { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font: 10px/1.3 var(--font-latin), sans-serif; }
|
|
@@ -2752,7 +2766,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2752
2766
|
.ai-context-distribution-row[data-key="skills"] { --distribution-color: var(--muted); }
|
|
2753
2767
|
.ai-context-distribution-row[data-key="context"] { --distribution-color: var(--green); }
|
|
2754
2768
|
.ai-context-distribution-row[data-key="left"] { --distribution-color: color-mix(in srgb, var(--muted) 55%, transparent); }
|
|
2755
|
-
.ai-send-button { display: grid; flex: 0 0 32px; place-items: center; width: 32px; min-width: 32px; height: 32px; padding: 0; border: 0; border-radius: 4px; background: var(--accent); color: #fff; box-shadow: 0 4px 12px rgba(139,61,44,.2); }
|
|
2769
|
+
.ai-send-button { display: grid; flex: 0 0 32px; place-items: center; width: 32px; min-width: 32px; min-height: 32px; height: 32px; padding: 0; border: 0; border-radius: 4px; background: var(--accent); color: #fff; box-shadow: 0 4px 12px rgba(139,61,44,.2); }
|
|
2756
2770
|
.ai-send-button:hover, .ai-send-button:focus-visible { background: var(--accent-dark); outline: 0; }
|
|
2757
2771
|
.ai-send-button:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 24%, transparent), 0 4px 12px rgba(139,61,44,.2); }
|
|
2758
2772
|
.ai-send-button:disabled { cursor: wait; opacity: .62; }
|
|
@@ -3655,7 +3669,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
3655
3669
|
overflow-x: hidden;
|
|
3656
3670
|
overflow-y: hidden;
|
|
3657
3671
|
}
|
|
3672
|
+
.app-shell:not(.shelf-mode):not(.ai-workspace-mode) { grid-template-columns: minmax(0, 1fr); }
|
|
3658
3673
|
.app-shell.ai-hidden-mode:not(.shelf-mode) { grid-template-columns: minmax(0, 1fr); }
|
|
3674
|
+
.app-shell.ai-workspace-mode > :is(.topbar, .left-panel, .main-panel) { visibility: hidden; pointer-events: none; }
|
|
3659
3675
|
.app-shell.shelf-mode {
|
|
3660
3676
|
grid-template-rows: auto minmax(0, 1fr);
|
|
3661
3677
|
min-height: 100dvh;
|
|
@@ -3705,14 +3721,14 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
3705
3721
|
top: 0;
|
|
3706
3722
|
bottom: 0;
|
|
3707
3723
|
right: 0;
|
|
3708
|
-
width:
|
|
3724
|
+
width: 100%;
|
|
3709
3725
|
grid-column: auto;
|
|
3710
3726
|
grid-row: auto;
|
|
3711
3727
|
min-width: 0;
|
|
3712
3728
|
padding: 0;
|
|
3713
|
-
border-left:
|
|
3729
|
+
border-left: 0;
|
|
3714
3730
|
border-bottom: 1px solid var(--line);
|
|
3715
|
-
box-shadow:
|
|
3731
|
+
box-shadow: none;
|
|
3716
3732
|
overflow: hidden;
|
|
3717
3733
|
transform: translateX(0);
|
|
3718
3734
|
transition: transform .2s ease;
|
|
@@ -3993,7 +4009,7 @@ body { font-size: var(--ui-font-size); }
|
|
|
3993
4009
|
.guard-card strong, .guard-card p, .message-actions button { font-size: calc(10px * var(--ai-font-scale)); }
|
|
3994
4010
|
.ai-context-warning strong { font-size: calc(11px * var(--ai-font-scale)); }
|
|
3995
4011
|
.ai-context-warning p, .ai-context-warning button { font-size: calc(9px * var(--ai-font-scale)); }
|
|
3996
|
-
.prompt-options select, .
|
|
4012
|
+
.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)); }
|
|
3997
4013
|
.ai-mention-option small { font-size: calc(8px * var(--ai-font-scale)); }
|
|
3998
4014
|
.ai-prompt { font-size: var(--ai-font-size); }
|
|
3999
4015
|
.ai-prompt-reference { font-size: calc(10px * var(--ai-font-scale)); }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type WorkPermissionModule = "prose" | "drafts" | "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews" | "ai-chat" | "ai-analysis" | "ai-settings";
|
|
1
|
+
export type WorkPermissionModule = "prose" | "comments" | "todos" | "drafts" | "settings" | "characters" | "races" | "organizations" | "timeline" | "relationships" | "outlines" | "reviews" | "ai-chat" | "ai-analysis" | "ai-settings";
|
|
2
2
|
export type WorkUiModule = "editor" | "drafts" | "settings" | "characters" | "races" | "organizations" | "timeline" | "comments" | "relationships" | "outlines" | "reviews" | "tasks" | "ai-settings";
|
|
3
3
|
export type WorkModuleAccess = "none" | "read" | "write";
|
|
4
4
|
export type WorkModulePermissions = Record<WorkPermissionModule, WorkModuleAccess>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export const WORK_PERMISSION_MODULES = Object.freeze([
|
|
2
2
|
{ id: "prose", uiModule: "editor", label: "正文" },
|
|
3
|
+
{ id: "comments", uiModule: "comments", label: "正文评论" },
|
|
4
|
+
{ id: "todos", uiModule: null, label: "正文待办" },
|
|
3
5
|
{ id: "drafts", uiModule: "drafts", label: "想法" },
|
|
4
6
|
{ id: "settings", uiModule: "settings", label: "设定库" },
|
|
5
7
|
{ id: "characters", uiModule: "characters", label: "角色" },
|
|
@@ -22,6 +24,8 @@ const validAccess = new Set(["none", "read", "write"]);
|
|
|
22
24
|
function migrateLegacyModulePermissions(value) {
|
|
23
25
|
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
24
26
|
const migrated = { ...value };
|
|
27
|
+
if (!validAccess.has(migrated.comments)) migrated.comments = validAccess.has(migrated.prose) ? migrated.prose : "none";
|
|
28
|
+
if (!validAccess.has(migrated.todos)) migrated.todos = validAccess.has(migrated.prose) ? migrated.prose : "none";
|
|
25
29
|
if (!validAccess.has(migrated.drafts)) {
|
|
26
30
|
const prose = validAccess.has(migrated.prose) ? migrated.prose : "none";
|
|
27
31
|
const settings = validAccess.has(migrated.settings) ? migrated.settings : "none";
|
|
@@ -83,10 +87,16 @@ export function canWritePermissionModule(work, moduleId) {
|
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
export function canReadUiModule(work, uiModule) {
|
|
90
|
+
if (uiModule === "comments") {
|
|
91
|
+
return canReadPermissionModule(work, "comments") || canReadPermissionModule(work, "todos");
|
|
92
|
+
}
|
|
86
93
|
return canReadPermissionModule(work, permissionModuleForUiModule(uiModule));
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
export function canWriteUiModule(work, uiModule) {
|
|
97
|
+
if (uiModule === "comments") {
|
|
98
|
+
return canWritePermissionModule(work, "comments") || canWritePermissionModule(work, "todos");
|
|
99
|
+
}
|
|
90
100
|
return canWritePermissionModule(work, permissionModuleForUiModule(uiModule));
|
|
91
101
|
}
|
|
92
102
|
|
|
@@ -96,10 +106,12 @@ export function firstReadableUiModule(work) {
|
|
|
96
106
|
|
|
97
107
|
export function permissionSummary(value) {
|
|
98
108
|
const permissions = normalizeModulePermissions(value, "custom");
|
|
99
|
-
const
|
|
109
|
+
const addable = WORK_PERMISSION_MODULES.filter((item) => ["comments", "todos"].includes(item.id) && permissions[item.id] === "write").map((item) => item.label);
|
|
110
|
+
const writable = WORK_PERMISSION_MODULES.filter((item) => !["comments", "todos"].includes(item.id) && permissions[item.id] === "write").map((item) => item.label);
|
|
100
111
|
const readable = WORK_PERMISSION_MODULES.filter((item) => permissions[item.id] === "read").map((item) => item.label);
|
|
101
112
|
const parts = [];
|
|
102
113
|
if (writable.length) parts.push(`可编辑:${writable.join("、")}`);
|
|
114
|
+
if (addable.length) parts.push(`可添加:${addable.join("、")}`);
|
|
103
115
|
if (readable.length) parts.push(`只读:${readable.join("、")}`);
|
|
104
116
|
if (!parts.length) parts.push("未授权任何模块");
|
|
105
117
|
return parts.join(";");
|
package/dist/store.js
CHANGED
|
@@ -2161,6 +2161,7 @@ export class Store {
|
|
|
2161
2161
|
note: requiredString(row, "note"),
|
|
2162
2162
|
status: requiredString(row, "status"),
|
|
2163
2163
|
versionNo: numberValue(row, "version_no"),
|
|
2164
|
+
createdByUserId: optionalString(row, "created_by_user_id"),
|
|
2164
2165
|
actor: optionalString(row, "actor_display_name") ?? optionalString(row, "actor_username") ?? "历史数据",
|
|
2165
2166
|
createdAt: requiredString(row, "created_at"),
|
|
2166
2167
|
updatedAt: requiredString(row, "updated_at")
|
|
@@ -2189,47 +2190,86 @@ export class Store {
|
|
|
2189
2190
|
throw notFound("章节批注");
|
|
2190
2191
|
return { ...this.mapChapterAnnotation(row), deletedAt: optionalString(row, "deleted_at") };
|
|
2191
2192
|
}
|
|
2192
|
-
listChapterAnnotations(chapterId) {
|
|
2193
|
+
listChapterAnnotations(chapterId, kinds, line) {
|
|
2193
2194
|
this.getChapter(chapterId);
|
|
2195
|
+
const kindFilter = kinds === undefined
|
|
2196
|
+
? { sql: "", params: [] }
|
|
2197
|
+
: kinds.length > 0
|
|
2198
|
+
? { sql: ` AND annotation.kind IN (${kinds.map(() => "?").join(",")})`, params: [...kinds] }
|
|
2199
|
+
: { sql: " AND 1 = 0", params: [] };
|
|
2200
|
+
const lineFilter = line === undefined
|
|
2201
|
+
? { sql: "", params: [] }
|
|
2202
|
+
: { sql: " AND annotation.start_line <= ? AND annotation.end_line >= ?", params: [line, line] };
|
|
2194
2203
|
return this.db.all(`SELECT annotation.*, user.display_name AS actor_display_name, user.username AS actor_username
|
|
2195
2204
|
FROM chapter_annotations annotation LEFT JOIN users user ON user.id = annotation.updated_by_user_id
|
|
2196
|
-
WHERE annotation.chapter_id = ? AND annotation.deleted_at IS NULL
|
|
2197
|
-
ORDER BY CASE annotation.status WHEN 'open' THEN 0 ELSE 1 END, annotation.start_line, annotation.created_at`, chapterId).map((row) => this.mapChapterAnnotation(row));
|
|
2205
|
+
WHERE annotation.chapter_id = ? AND annotation.deleted_at IS NULL${kindFilter.sql}${lineFilter.sql}
|
|
2206
|
+
ORDER BY CASE annotation.status WHEN 'open' THEN 0 ELSE 1 END, annotation.start_line, annotation.created_at`, chapterId, ...kindFilter.params, ...lineFilter.params).map((row) => this.mapChapterAnnotation(row));
|
|
2198
2207
|
}
|
|
2199
|
-
|
|
2208
|
+
listChapterAnnotationCounts(chapterId, kinds) {
|
|
2209
|
+
this.getChapter(chapterId);
|
|
2210
|
+
const kindFilter = kinds === undefined
|
|
2211
|
+
? { sql: "", params: [] }
|
|
2212
|
+
: kinds.length > 0
|
|
2213
|
+
? { sql: ` AND annotation.kind IN (${kinds.map(() => "?").join(",")})`, params: [...kinds] }
|
|
2214
|
+
: { sql: " AND 1 = 0", params: [] };
|
|
2215
|
+
return this.db.all(`WITH RECURSIVE annotation_lines(line, end_line) AS (
|
|
2216
|
+
SELECT annotation.start_line, annotation.end_line
|
|
2217
|
+
FROM chapter_annotations annotation
|
|
2218
|
+
WHERE annotation.chapter_id = ? AND annotation.deleted_at IS NULL${kindFilter.sql}
|
|
2219
|
+
UNION ALL
|
|
2220
|
+
SELECT line + 1, end_line
|
|
2221
|
+
FROM annotation_lines
|
|
2222
|
+
WHERE line < end_line
|
|
2223
|
+
)
|
|
2224
|
+
SELECT line, COUNT(*) AS count
|
|
2225
|
+
FROM annotation_lines
|
|
2226
|
+
GROUP BY line
|
|
2227
|
+
ORDER BY line`, chapterId, ...kindFilter.params).map((row) => ({ line: Number(row.line), count: Number(row.count) }));
|
|
2228
|
+
}
|
|
2229
|
+
listWorkChapterAnnotations(workId, kinds) {
|
|
2200
2230
|
this.getWork(workId);
|
|
2231
|
+
const kindFilter = kinds === undefined
|
|
2232
|
+
? { sql: "", params: [] }
|
|
2233
|
+
: kinds.length > 0
|
|
2234
|
+
? { sql: ` AND annotation.kind IN (${kinds.map(() => "?").join(",")})`, params: [...kinds] }
|
|
2235
|
+
: { sql: " AND 1 = 0", params: [] };
|
|
2201
2236
|
return this.db.all(`SELECT annotation.*, user.display_name AS actor_display_name, user.username AS actor_username,
|
|
2202
2237
|
chapter.title AS chapter_title, volume.title AS volume_title
|
|
2203
2238
|
FROM chapter_annotations annotation
|
|
2204
2239
|
JOIN chapters chapter ON chapter.id = annotation.chapter_id
|
|
2205
2240
|
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
2206
2241
|
LEFT JOIN users user ON user.id = annotation.updated_by_user_id
|
|
2207
|
-
WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL
|
|
2242
|
+
WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL${kindFilter.sql}
|
|
2208
2243
|
ORDER BY CASE annotation.status WHEN 'open' THEN 0 ELSE 1 END,
|
|
2209
2244
|
volume.sort_order, volume.created_at, chapter.sort_order, chapter.created_at,
|
|
2210
|
-
annotation.start_line, annotation.created_at`, workId).map((row) => ({
|
|
2245
|
+
annotation.start_line, annotation.created_at`, workId, ...kindFilter.params).map((row) => ({
|
|
2211
2246
|
...this.mapChapterAnnotation(row),
|
|
2212
2247
|
volumeTitle: requiredString(row, "volume_title"),
|
|
2213
2248
|
chapterTitle: requiredString(row, "chapter_title")
|
|
2214
2249
|
}));
|
|
2215
2250
|
}
|
|
2216
|
-
listWorkChapterAnnotationsPage(workId, pagination) {
|
|
2251
|
+
listWorkChapterAnnotationsPage(workId, pagination, kinds) {
|
|
2217
2252
|
this.getWork(workId);
|
|
2218
2253
|
const page = paginationSql(pagination);
|
|
2254
|
+
const kindFilter = kinds === undefined
|
|
2255
|
+
? { sql: "", params: [] }
|
|
2256
|
+
: kinds.length > 0
|
|
2257
|
+
? { sql: ` AND annotation.kind IN (${kinds.map(() => "?").join(",")})`, params: [...kinds] }
|
|
2258
|
+
: { sql: " AND 1 = 0", params: [] };
|
|
2219
2259
|
const rows = this.db.all(`SELECT annotation.*, user.display_name AS actor_display_name, user.username AS actor_username,
|
|
2220
2260
|
chapter.title AS chapter_title, volume.title AS volume_title
|
|
2221
2261
|
FROM chapter_annotations annotation
|
|
2222
2262
|
JOIN chapters chapter ON chapter.id = annotation.chapter_id
|
|
2223
2263
|
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
2224
2264
|
LEFT JOIN users user ON user.id = annotation.updated_by_user_id
|
|
2225
|
-
WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL
|
|
2265
|
+
WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL${kindFilter.sql}
|
|
2226
2266
|
ORDER BY CASE annotation.status WHEN 'open' THEN 0 ELSE 1 END,
|
|
2227
2267
|
volume.sort_order, volume.created_at, chapter.sort_order, chapter.created_at,
|
|
2228
|
-
annotation.start_line, annotation.created_at${page.sql}`, workId, ...page.params);
|
|
2268
|
+
annotation.start_line, annotation.created_at${page.sql}`, workId, ...kindFilter.params, ...page.params);
|
|
2229
2269
|
const total = numberValue(this.db.get(`SELECT COUNT(*) AS count
|
|
2230
2270
|
FROM chapter_annotations annotation
|
|
2231
2271
|
JOIN chapters chapter ON chapter.id = annotation.chapter_id
|
|
2232
|
-
WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL`, workId) ?? {}, "count");
|
|
2272
|
+
WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL${kindFilter.sql}`, workId, ...kindFilter.params) ?? {}, "count");
|
|
2233
2273
|
return paginated(rows.map((row) => ({
|
|
2234
2274
|
...this.mapChapterAnnotation(row),
|
|
2235
2275
|
volumeTitle: requiredString(row, "volume_title"),
|
|
@@ -5521,6 +5561,7 @@ export class Store {
|
|
|
5521
5561
|
return {
|
|
5522
5562
|
workId,
|
|
5523
5563
|
roleplayCharacterId: optionalString(conversation, "roleplay_character_id"),
|
|
5564
|
+
roleplayUserCharacterId: optionalString(conversation, "roleplay_user_character_id"),
|
|
5524
5565
|
summary: requiredString(conversation, "compacted_summary"),
|
|
5525
5566
|
compactedMessageCount,
|
|
5526
5567
|
totalMessageCount,
|
|
@@ -5664,14 +5705,27 @@ export class Store {
|
|
|
5664
5705
|
this.audit(workId, "ai-conversation.deleted", "ai-conversation", conversationId, {});
|
|
5665
5706
|
});
|
|
5666
5707
|
}
|
|
5667
|
-
setAiConversationRoleplayCharacter(conversationId, characterId) {
|
|
5708
|
+
setAiConversationRoleplayCharacter(conversationId, characterId, userCharacterId) {
|
|
5668
5709
|
const conversation = this.db.get("SELECT * FROM ai_conversations WHERE id = ?", conversationId);
|
|
5669
5710
|
if (!conversation)
|
|
5670
5711
|
throw notFound("AI 对话");
|
|
5671
5712
|
const workId = requiredString(conversation, "work_id");
|
|
5672
5713
|
const previousCharacterId = optionalString(conversation, "roleplay_character_id");
|
|
5673
|
-
|
|
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) {
|
|
5674
5727
|
return this.getAiConversationSummary(conversationId);
|
|
5728
|
+
}
|
|
5675
5729
|
const messageCount = Number(this.db.get("SELECT COUNT(*) AS count FROM ai_conversation_messages WHERE conversation_id = ?", conversationId)?.count ?? 0);
|
|
5676
5730
|
if (messageCount > 0) {
|
|
5677
5731
|
throw new AppError(409, previousCharacterId ? "ROLEPLAY_CHARACTER_LOCKED" : "ROLEPLAY_CONVERSATION_STARTED", previousCharacterId ? "角色扮演对话开始后不能退出模式或更换角色卡" : "当前对话已经开始,不能中途切换为角色扮演");
|
|
@@ -5685,11 +5739,22 @@ export class Store {
|
|
|
5685
5739
|
throw new AppError(409, "ROLEPLAY_CHARACTER_MERGED", "已合并角色不能用于角色扮演");
|
|
5686
5740
|
}
|
|
5687
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
|
+
}
|
|
5688
5751
|
this.db.transaction(() => {
|
|
5689
|
-
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);
|
|
5690
5753
|
this.audit(workId, "ai-conversation.roleplay-updated", "ai-conversation", conversationId, {
|
|
5691
5754
|
previousCharacterId,
|
|
5692
|
-
characterId
|
|
5755
|
+
characterId,
|
|
5756
|
+
previousUserCharacterId,
|
|
5757
|
+
userCharacterId: nextUserCharacterId
|
|
5693
5758
|
});
|
|
5694
5759
|
});
|
|
5695
5760
|
return this.getAiConversationSummary(conversationId);
|
|
@@ -5708,7 +5773,7 @@ export class Store {
|
|
|
5708
5773
|
throw new AppError(409, "AI_CONVERSATION_TASK_LOCKED", "对话开始后不能切换任务类型");
|
|
5709
5774
|
}
|
|
5710
5775
|
this.db.transaction(() => {
|
|
5711
|
-
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);
|
|
5712
5777
|
this.audit(workId, "ai-conversation.task-type-updated", "ai-conversation", conversationId, {
|
|
5713
5778
|
previousTaskType,
|
|
5714
5779
|
taskType
|
|
@@ -6001,7 +6066,7 @@ export class Store {
|
|
|
6001
6066
|
?? JSON.stringify(EMPTY_AI_INJECTED_ENTITIES);
|
|
6002
6067
|
const systemClockText = optionalString(conversation, "system_clock_text") ?? "";
|
|
6003
6068
|
this.db.transaction(() => {
|
|
6004
|
-
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
|
|
6005
6070
|
? JSON.stringify(normalizeWorkAgentTools(this.getWorkAiSettings(workId).agentTools))
|
|
6006
6071
|
: String(conversation.agent_tools_json), injectedEntitiesJson, systemClockText, timestamp, timestamp, currentRequestActor()?.userId ?? null);
|
|
6007
6072
|
for (const message of messages.slice(0, targetIndex + 1)) {
|
|
@@ -6044,6 +6109,7 @@ export class Store {
|
|
|
6044
6109
|
}
|
|
6045
6110
|
mapAiConversation(row) {
|
|
6046
6111
|
const roleplayCharacterId = optionalString(row, "roleplay_character_id");
|
|
6112
|
+
const roleplayUserCharacterId = optionalString(row, "roleplay_user_character_id");
|
|
6047
6113
|
const firstUserMessage = this.db.get(`SELECT metadata_json FROM ai_conversation_messages
|
|
6048
6114
|
WHERE conversation_id = ? AND role = 'user'
|
|
6049
6115
|
ORDER BY created_at, rowid
|
|
@@ -6057,6 +6123,9 @@ export class Store {
|
|
|
6057
6123
|
const roleplayCharacter = roleplayCharacterId
|
|
6058
6124
|
? this.db.get("SELECT id, name, code FROM characters WHERE id = ? AND work_id = ?", roleplayCharacterId, requiredString(row, "work_id"))
|
|
6059
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;
|
|
6060
6129
|
return {
|
|
6061
6130
|
id: requiredString(row, "id"),
|
|
6062
6131
|
workId: requiredString(row, "work_id"),
|
|
@@ -6075,6 +6144,11 @@ export class Store {
|
|
|
6075
6144
|
name: requiredString(roleplayCharacter, "name"),
|
|
6076
6145
|
code: requiredString(roleplayCharacter, "code")
|
|
6077
6146
|
} : null,
|
|
6147
|
+
roleplayUserCharacter: roleplayUserCharacter ? {
|
|
6148
|
+
id: requiredString(roleplayUserCharacter, "id"),
|
|
6149
|
+
name: requiredString(roleplayUserCharacter, "name"),
|
|
6150
|
+
code: requiredString(roleplayUserCharacter, "code")
|
|
6151
|
+
} : null,
|
|
6078
6152
|
agentTools: row.agent_tools_json == null || row.agent_tools_json === undefined
|
|
6079
6153
|
? null
|
|
6080
6154
|
: normalizeWorkAgentTools(row.agent_tools_json),
|