@musnows/scriverse 0.7.12 → 0.7.13
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.js +86 -35
- package/dist/ai.js.map +1 -1
- package/dist/public/app.js +206 -80
- package/dist/public/index.html +4 -4
- package/dist/public/stream-typewriter.d.ts +12 -1
- package/dist/public/stream-typewriter.js +65 -5
- package/dist/public/styles.css +73 -17
- package/dist/store.js +187 -31
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -3,15 +3,69 @@ const FINISHING_CHARACTERS_PER_FRAME = 2;
|
|
|
3
3
|
const FINISHING_ACCELERATION = 0.9;
|
|
4
4
|
const MAX_STREAMING_CHARACTERS_PER_FRAME = 12;
|
|
5
5
|
const MAX_FINISHING_CHARACTERS_PER_FRAME = 24;
|
|
6
|
+
const STREAMING_FRAME_RATE = 60;
|
|
7
|
+
const DEFAULT_STREAMING_CHARACTERS_PER_SECOND = STREAMING_CHARACTERS_PER_FRAME * STREAMING_FRAME_RATE;
|
|
8
|
+
const MIN_STREAMING_CHARACTERS_PER_SECOND = DEFAULT_STREAMING_CHARACTERS_PER_SECOND;
|
|
9
|
+
const MAX_STREAMING_CHARACTERS_PER_SECOND = MAX_STREAMING_CHARACTERS_PER_FRAME * STREAMING_FRAME_RATE;
|
|
10
|
+
const SPEED_SAMPLE_MAX_GAP_MS = 1_000;
|
|
11
|
+
const SPEED_SMOOTHING = 0.25;
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
function clampCharactersPerSecond(value) {
|
|
14
|
+
const speed = Number(value);
|
|
15
|
+
if (!Number.isFinite(speed)) return DEFAULT_STREAMING_CHARACTERS_PER_SECOND;
|
|
16
|
+
return Math.min(MAX_STREAMING_CHARACTERS_PER_SECOND, Math.max(MIN_STREAMING_CHARACTERS_PER_SECOND, speed));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function currentTime() {
|
|
20
|
+
return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createStreamTypewriterSpeedController({
|
|
24
|
+
now = currentTime,
|
|
25
|
+
initialCharactersPerSecond = DEFAULT_STREAMING_CHARACTERS_PER_SECOND
|
|
26
|
+
} = {}) {
|
|
27
|
+
let charactersPerSecond = clampCharactersPerSecond(initialCharactersPerSecond);
|
|
28
|
+
let lastObservedAt = null;
|
|
29
|
+
|
|
30
|
+
const update = (observedCharactersPerSecond) => {
|
|
31
|
+
const observed = clampCharactersPerSecond(observedCharactersPerSecond);
|
|
32
|
+
charactersPerSecond = clampCharactersPerSecond(
|
|
33
|
+
charactersPerSecond + (observed - charactersPerSecond) * SPEED_SMOOTHING
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
observe(characterCount) {
|
|
39
|
+
const characters = Math.max(0, Math.floor(Number(characterCount) || 0));
|
|
40
|
+
if (characters === 0) return;
|
|
41
|
+
const observedAt = Number(now());
|
|
42
|
+
if (Number.isFinite(observedAt) && lastObservedAt !== null) {
|
|
43
|
+
const elapsed = observedAt - lastObservedAt;
|
|
44
|
+
if (elapsed > 0 && elapsed <= SPEED_SAMPLE_MAX_GAP_MS) {
|
|
45
|
+
update(characters / elapsed * 1_000);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (Number.isFinite(observedAt)) lastObservedAt = observedAt;
|
|
49
|
+
|
|
50
|
+
// 单次收到的大块内容也需要让同一条流的后续轮次继承当前的追赶速度。
|
|
51
|
+
const backlogSpeed = Math.ceil(characters / 30) * STREAMING_FRAME_RATE;
|
|
52
|
+
if (backlogSpeed > charactersPerSecond) charactersPerSecond = Math.min(MAX_STREAMING_CHARACTERS_PER_SECOND, backlogSpeed);
|
|
53
|
+
},
|
|
54
|
+
charactersPerSecond() {
|
|
55
|
+
return charactersPerSecond;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function streamTypewriterBatchSize(pendingCharacters, finishing = false, charactersPerSecond = DEFAULT_STREAMING_CHARACTERS_PER_SECOND) {
|
|
8
61
|
const pending = Math.max(0, Math.floor(Number(pendingCharacters) || 0));
|
|
9
62
|
if (pending === 0) return 0;
|
|
10
63
|
const minimum = finishing ? FINISHING_CHARACTERS_PER_FRAME : STREAMING_CHARACTERS_PER_FRAME;
|
|
11
64
|
const maximum = finishing ? MAX_FINISHING_CHARACTERS_PER_FRAME : MAX_STREAMING_CHARACTERS_PER_FRAME;
|
|
65
|
+
const speedBased = Math.ceil(clampCharactersPerSecond(charactersPerSecond) / STREAMING_FRAME_RATE);
|
|
12
66
|
const adaptive = finishing
|
|
13
|
-
? Math.ceil(Math.sqrt(pending) * FINISHING_ACCELERATION)
|
|
14
|
-
: Math.ceil(pending / 30);
|
|
67
|
+
? Math.max(Math.ceil(Math.sqrt(pending) * FINISHING_ACCELERATION), speedBased)
|
|
68
|
+
: Math.max(Math.ceil(pending / 30), speedBased);
|
|
15
69
|
return Math.min(pending, maximum, Math.max(minimum, adaptive));
|
|
16
70
|
}
|
|
17
71
|
|
|
@@ -19,7 +73,8 @@ export function createStreamTypewriter({
|
|
|
19
73
|
onRender,
|
|
20
74
|
scheduleFrame = (callback) => window.requestAnimationFrame(callback),
|
|
21
75
|
cancelFrame = (handle) => window.cancelAnimationFrame(handle),
|
|
22
|
-
reducedMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true
|
|
76
|
+
reducedMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true,
|
|
77
|
+
speedController = null
|
|
23
78
|
}) {
|
|
24
79
|
if (typeof onRender !== "function") throw new TypeError("onRender must be a function");
|
|
25
80
|
|
|
@@ -47,7 +102,11 @@ export function createStreamTypewriter({
|
|
|
47
102
|
scheduledFrame = null;
|
|
48
103
|
const batchSize = reducedMotion
|
|
49
104
|
? pendingCharacters.length
|
|
50
|
-
: streamTypewriterBatchSize(
|
|
105
|
+
: streamTypewriterBatchSize(
|
|
106
|
+
pendingCharacters.length,
|
|
107
|
+
finishing,
|
|
108
|
+
speedController?.charactersPerSecond?.()
|
|
109
|
+
);
|
|
51
110
|
visibleCharacters.push(...pendingCharacters.splice(0, batchSize));
|
|
52
111
|
render();
|
|
53
112
|
if (pendingCharacters.length) schedule();
|
|
@@ -59,6 +118,7 @@ export function createStreamTypewriter({
|
|
|
59
118
|
append(value) {
|
|
60
119
|
const characters = Array.from(String(value ?? ""));
|
|
61
120
|
if (!characters.length) return;
|
|
121
|
+
speedController?.observe?.(characters.length);
|
|
62
122
|
pendingCharacters.push(...characters);
|
|
63
123
|
schedule();
|
|
64
124
|
},
|
package/dist/public/styles.css
CHANGED
|
@@ -1788,6 +1788,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1788
1788
|
.character-extraction-editor-toolbar { justify-content: flex-end; }
|
|
1789
1789
|
.character-extraction-editor-toolbar > span { margin-right: auto; color: var(--accent-dark); font-weight: 600; }
|
|
1790
1790
|
.character-extraction-editor-toolbar button { padding: 6px 9px; font-size: 9px; }
|
|
1791
|
+
.character-extraction-apply-button { flex: 0 0 auto; }
|
|
1792
|
+
.character-extraction-apply-note { margin: -5px 0 0; color: var(--muted); font-size: 9px; line-height: 1.5; }
|
|
1791
1793
|
.character-extraction-candidate-list { display: grid; gap: 9px; min-width: 0; }
|
|
1792
1794
|
.character-extraction-candidate { display: grid; gap: 10px; min-width: 0; padding: 11px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); transition: opacity .15s ease, border-color .15s ease; }
|
|
1793
1795
|
.character-extraction-candidate.is-skipped { opacity: .7; }
|
|
@@ -2582,7 +2584,11 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2582
2584
|
.ai-tool-call-info code, .ai-tool-call-info time { font-family: var(--font-latin), monospace; }
|
|
2583
2585
|
.ai-tool-call-detail section { min-width: 0; }
|
|
2584
2586
|
.ai-tool-call-detail h3 { margin: 0 0 7px; color: var(--muted); font-size: 10px; font-weight: 600; }
|
|
2585
|
-
.ai-tool-call-
|
|
2587
|
+
.ai-tool-call-code-block { position: relative; min-width: 0; }
|
|
2588
|
+
.ai-tool-call-detail pre { min-height: 120px; max-height: 32vh; overflow: auto; margin: 0; padding: 30px 12px 12px; border: 1px solid var(--line); border-radius: 5px; background: var(--paper-deep); color: var(--ink); font: 10px/1.55 var(--font-latin), monospace; white-space: pre-wrap; word-break: break-word; }
|
|
2589
|
+
.ai-tool-call-copy-button { position: absolute; z-index: 1; top: 6px; right: 6px; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; min-height: 0; padding: 0; border: 1px solid transparent; border-radius: 4px; background: color-mix(in srgb, var(--paper-deep) 86%, var(--ink)); color: var(--muted); }
|
|
2590
|
+
.ai-tool-call-copy-button svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
|
|
2591
|
+
.ai-tool-call-copy-button:hover, .ai-tool-call-copy-button:focus-visible, .ai-tool-call-copy-button.is-copied { border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); background: color-mix(in srgb, var(--accent) 12%, var(--paper-deep)); color: var(--accent-dark); outline: none; }
|
|
2586
2592
|
#ai-tool-call-arguments { min-height: 0; max-height: 26vh; }
|
|
2587
2593
|
.ai-tool-call-detail section:last-child pre { min-height: 180px; }
|
|
2588
2594
|
@media (max-width: 680px) { .ai-tool-call-info { grid-template-columns: minmax(0, 1fr); }.ai-tool-call-detail pre { min-height: 100px; max-height: 26vh; }#ai-tool-call-arguments { min-height: 0; max-height: 20vh; }.ai-tool-call-detail section:last-child pre { min-height: 140px; } }
|
|
@@ -2701,6 +2707,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2701
2707
|
line-height: 1.55;
|
|
2702
2708
|
animation: toast-appear-top .18s ease;
|
|
2703
2709
|
}
|
|
2710
|
+
.toast.ai-new-conversation-toast { white-space: pre-line; }
|
|
2704
2711
|
.toast-region[data-position="bottom-right"] .toast { animation-name: toast-appear-bottom; }
|
|
2705
2712
|
.toast.error {
|
|
2706
2713
|
border-color: color-mix(in srgb, var(--toast-error-fg) 16%, transparent);
|
|
@@ -2835,6 +2842,10 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2835
2842
|
.dialog-fields .form-field-note { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.65; }
|
|
2836
2843
|
.dialog-fields label { display: grid; gap: 6px; color: var(--muted); font-size: 11px; }
|
|
2837
2844
|
.dialog-fields input, .dialog-fields textarea, .dialog-fields select { width: 100%; padding: 9px 10px; font-size: 13px; }
|
|
2845
|
+
.dialog-fields select { padding-inline-end: 28px; }
|
|
2846
|
+
.dialog-fields .character-extraction-candidate-toggle { display: inline-flex; width: auto; align-items: center; gap: 7px; margin: 0; color: var(--ink); font-size: 9px; white-space: nowrap; }
|
|
2847
|
+
.dialog-fields .character-extraction-candidate-toggle input[type="checkbox"] { flex: 0 0 18px; width: 18px; min-width: 18px; height: 18px; padding: 0; }
|
|
2848
|
+
.dialog-fields .character-extraction-candidate-toggle input[type="checkbox"]:focus-visible { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
|
|
2838
2849
|
.dialog-fields textarea { resize: vertical; min-height: 90px; line-height: 1.6; }
|
|
2839
2850
|
.system-restart-dialog { width: min(500px, 92vw); }
|
|
2840
2851
|
.system-restart-message { margin: 0; color: var(--ink); font-size: 13px; line-height: 1.7; }
|
|
@@ -2880,30 +2891,72 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2880
2891
|
.task-chapter-field select:disabled { cursor: not-allowed; }
|
|
2881
2892
|
.task-scope-field { min-width: 0; transition: opacity .15s ease; }
|
|
2882
2893
|
.task-scope-field.is-disabled { opacity: .48; }
|
|
2894
|
+
.task-scope-field-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; min-width: 0; color: var(--muted); font-size: 11px; }
|
|
2895
|
+
.task-scope-field-heading > span { min-width: 0; }
|
|
2896
|
+
.task-scope-field-heading small { color: var(--muted); font-size: 9px; }
|
|
2883
2897
|
.task-scope-picker { display: grid; gap: 8px; min-width: 0; }
|
|
2884
|
-
.dialog-fields .task-scope-trigger { display: flex; width: 100%; min-height: 42px; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 11px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 12px; text-align: left; }
|
|
2898
|
+
.dialog-fields .task-scope-trigger { display: flex; width: 100%; min-height: 42px; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 11px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 12px; line-height: 1.2; text-align: left; }
|
|
2885
2899
|
.task-scope-trigger:hover, .task-scope-trigger:focus-visible, .task-scope-trigger[aria-expanded="true"] { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 12%, transparent); outline: none; }
|
|
2886
2900
|
.task-scope-trigger:disabled { cursor: not-allowed; opacity: .48; }
|
|
2887
2901
|
.task-scope-trigger > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2888
|
-
.task-scope-trigger-meta { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 8px; color: var(--muted); font-family: var(--font-latin), monospace; font-size: 10px; }
|
|
2889
|
-
.task-scope-chevron { font-size: 15px; line-height: 1; transition: transform .15s ease; }
|
|
2902
|
+
.task-scope-trigger-meta { display: inline-flex; flex: 0 0 auto; height: 18px; align-items: center; gap: 8px; color: var(--muted); font-family: var(--font-latin), monospace; font-size: 10px; line-height: 1; }
|
|
2903
|
+
.task-scope-chevron { display: inline-grid; width: 16px; height: 18px; place-items: center; font-size: 15px; line-height: 1; transition: transform .15s ease; }
|
|
2890
2904
|
.task-scope-trigger[aria-expanded="true"] .task-scope-chevron { transform: rotate(180deg); }
|
|
2891
|
-
.task-scope-
|
|
2892
|
-
.task-scope-
|
|
2893
|
-
.
|
|
2894
|
-
.
|
|
2895
|
-
.task-scope-
|
|
2896
|
-
.task-scope-
|
|
2897
|
-
.task-scope-
|
|
2898
|
-
.task-scope-
|
|
2899
|
-
.
|
|
2905
|
+
.task-scope-panel { display: grid; gap: 14px; min-width: 0; padding: 14px; border: 1px solid color-mix(in srgb, var(--accent) 36%, var(--line)); border-radius: 8px; background: var(--surface-soft); box-shadow: 0 12px 30px rgba(48,39,31,.12); }
|
|
2906
|
+
.task-scope-panel.hidden { display: none; }
|
|
2907
|
+
.task-scope-panel-header, .task-scope-panel-section-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-width: 0; }
|
|
2908
|
+
.task-scope-panel-header > div, .task-scope-panel-section-header > div { display: grid; gap: 3px; min-width: 0; }
|
|
2909
|
+
.task-scope-panel-header strong, .task-scope-panel-section-header strong { color: var(--ink); font-size: 11px; font-weight: 650; }
|
|
2910
|
+
.task-scope-panel-header small { color: var(--muted); font-size: 9px; line-height: 1.5; }
|
|
2911
|
+
.task-scope-author-note-banner { display: grid; grid-template-columns: 20px minmax(0, 1fr); align-items: start; gap: 8px; padding: 9px 10px; border: 1px solid color-mix(in srgb, var(--accent) 34%, var(--line)); border-left: 3px solid var(--accent); border-radius: 6px; background: color-mix(in srgb, var(--accent) 8%, var(--surface)); color: var(--muted); font-size: 9px; line-height: 1.55; }
|
|
2912
|
+
.task-scope-author-note-banner-icon { display: grid; width: 18px; height: 18px; place-items: center; border-radius: 50%; background: var(--accent); color: #fff; font-family: var(--font-latin), monospace; font-size: 11px; font-weight: 700; line-height: 1; }
|
|
2913
|
+
.task-scope-author-note-banner div { display: grid; gap: 2px; min-width: 0; }
|
|
2914
|
+
.task-scope-author-note-banner strong { color: var(--accent-dark); font-size: 10px; }
|
|
2915
|
+
.task-scope-clear { flex: 0 0 auto; padding: 5px 7px; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--accent-dark); font-size: 9px; }
|
|
2916
|
+
.task-scope-clear:hover:not(:disabled), .task-scope-clear:focus-visible { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 7%, transparent); outline: none; }
|
|
2917
|
+
.task-scope-clear:disabled { cursor: default; opacity: .45; }
|
|
2918
|
+
.task-scope-panel-grid { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(230px, .8fr); gap: 12px; min-width: 0; }
|
|
2919
|
+
.task-scope-available, .task-scope-selected { display: grid; align-content: start; gap: 10px; min-width: 0; min-height: 360px; padding: 12px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); }
|
|
2920
|
+
.task-scope-panel-section-header > span { flex: 0 0 auto; color: var(--muted); font-family: var(--font-latin), monospace; font-size: 9px; }
|
|
2921
|
+
.task-scope-search { gap: 5px !important; }
|
|
2922
|
+
.task-scope-search > span { color: var(--muted); font-size: 9px; }
|
|
2923
|
+
.dialog-fields .task-scope-search input { min-height: 34px; padding: 6px 9px; background: var(--surface-soft); font-size: 10px; }
|
|
2924
|
+
.task-scope-options { display: grid; align-content: start; gap: 6px; max-height: min(42vh, 360px); overflow-y: auto; padding: 2px 4px 2px 0; }
|
|
2925
|
+
.task-scope-volume-group { display: grid; gap: 5px; min-width: 0; padding: 5px 0 7px; border-bottom: 1px solid color-mix(in srgb, var(--line) 72%, transparent); }
|
|
2926
|
+
.task-scope-volume-group:last-child { border-bottom: 0; }
|
|
2927
|
+
.task-scope-volume-group > header { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 0 2px 2px; }
|
|
2928
|
+
.task-scope-volume-group > header strong { overflow: hidden; color: var(--accent-dark); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
|
2929
|
+
.task-scope-volume-group > header span { flex: 0 0 auto; color: var(--muted); font-family: var(--font-latin), monospace; font-size: 8px; }
|
|
2930
|
+
.dialog-fields .task-scope-volume-option { position: relative; display: grid; grid-template-columns: 18px minmax(0, 1fr); align-items: center; gap: 8px; min-width: 0; margin: 0; color: var(--ink); cursor: pointer; }
|
|
2931
|
+
.task-scope-volume-option input { position: absolute; width: 1px !important; min-width: 1px; height: 1px; padding: 0; border: 0; opacity: 0; }
|
|
2932
|
+
.task-scope-volume-option:hover, .task-scope-volume-option:has(input:focus-visible) { color: var(--accent-dark); }
|
|
2933
|
+
.task-scope-volume-option:has(input:checked) .task-scope-checkbox, .task-scope-volume-option:has(input:indeterminate) .task-scope-checkbox { border-color: var(--accent); background: var(--accent); }
|
|
2934
|
+
.task-scope-volume-option:has(input:checked) .task-scope-checkbox::after { opacity: 1; transform: translateY(-1px) rotate(-45deg) scale(1); }
|
|
2935
|
+
.task-scope-volume-option input:indeterminate + .task-scope-checkbox::after { width: 8px; height: 2px; border: 0; background: #fff; opacity: 1; transform: none; }
|
|
2936
|
+
.task-scope-volume-copy { display: grid; gap: 2px; min-width: 0; }
|
|
2937
|
+
.task-scope-volume-copy strong { overflow: hidden; color: inherit !important; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
|
2938
|
+
.task-scope-volume-copy small { overflow: hidden; color: var(--muted); font-size: 8px; text-overflow: ellipsis; white-space: nowrap; }
|
|
2939
|
+
.dialog-fields .task-scope-option { position: relative; display: grid; grid-template-columns: 18px minmax(0, 1fr); align-items: center; gap: 8px; min-width: 0; padding: 8px 9px; border: 1px solid var(--line); border-radius: 5px; background: var(--surface-soft); color: var(--muted); cursor: pointer; transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease; }
|
|
2900
2940
|
.dialog-fields .task-scope-option.hidden { display: none; }
|
|
2901
|
-
.
|
|
2902
|
-
.task-scope-option
|
|
2941
|
+
.task-scope-option:hover, .task-scope-option:has(input:focus-visible) { border-color: color-mix(in srgb, var(--accent) 54%, var(--line)); }
|
|
2942
|
+
.task-scope-option input { position: absolute; width: 1px !important; min-width: 1px; height: 1px; padding: 0; border: 0; opacity: 0; }
|
|
2943
|
+
.task-scope-checkbox { display: inline-grid; width: 18px; height: 18px; place-items: center; border: 1px solid var(--line-strong); border-radius: 4px; background: var(--surface); transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease; }
|
|
2944
|
+
.task-scope-checkbox::after { width: 8px; height: 5px; border: solid #fff; border-width: 0 0 2px 2px; content: ""; opacity: 0; transform: translateY(-1px) rotate(-45deg) scale(.7); transition: opacity .12s ease, transform .12s ease; }
|
|
2903
2945
|
.task-scope-option strong { overflow: hidden; color: var(--ink); font-size: 10px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
|
|
2946
|
+
.task-scope-option-copy { display: grid; gap: 2px; min-width: 0; }
|
|
2904
2947
|
.task-scope-option small { overflow: hidden; color: var(--muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
|
|
2905
|
-
.task-scope-option:has(input:checked) { border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); background: color-mix(in srgb, var(--accent) 7%, var(--surface)); }
|
|
2906
|
-
.task-scope-option
|
|
2948
|
+
.task-scope-option:has(input:checked) { border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); background: color-mix(in srgb, var(--accent) 7%, var(--surface)); box-shadow: inset 2px 0 var(--accent); }
|
|
2949
|
+
.task-scope-option:has(input:checked) .task-scope-checkbox { border-color: var(--accent); background: var(--accent); }
|
|
2950
|
+
.task-scope-option:has(input:checked) .task-scope-checkbox::after { opacity: 1; transform: translateY(-1px) rotate(-45deg) scale(1); }
|
|
2951
|
+
.task-scope-selected-note { margin: -2px 0 0; color: var(--muted); font-size: 9px; line-height: 1.5; }
|
|
2952
|
+
.task-scope-selected-list { display: grid; align-content: start; gap: 6px; max-height: min(42vh, 360px); overflow-y: auto; padding: 2px 4px 2px 0; }
|
|
2953
|
+
.task-scope-selected-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; min-width: 0; padding: 8px 9px; border: 1px solid color-mix(in srgb, var(--accent) 32%, var(--line)); border-radius: 5px; background: color-mix(in srgb, var(--accent) 5%, var(--surface)); }
|
|
2954
|
+
.task-scope-selected-copy { display: grid; gap: 2px; min-width: 0; }
|
|
2955
|
+
.task-scope-selected-copy strong { overflow: hidden; color: var(--ink); font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
|
|
2956
|
+
.task-scope-selected-copy small { overflow: hidden; color: var(--muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
|
|
2957
|
+
.task-scope-selected-item button { display: grid; width: 24px; height: 24px; place-items: center; padding: 0; border: 1px solid transparent; border-radius: 50%; background: transparent; color: var(--muted); font-size: 16px; line-height: 1; }
|
|
2958
|
+
.task-scope-selected-item button:hover, .task-scope-selected-item button:focus-visible { border-color: var(--line); background: var(--surface); color: var(--accent-dark); outline: none; }
|
|
2959
|
+
.task-scope-selected-empty { margin: 0; padding: 28px 12px; border: 1px dashed var(--line); color: var(--muted); font-size: 10px; line-height: 1.5; text-align: center; }
|
|
2907
2960
|
.task-scope-empty { margin: 0; padding: 18px 8px; color: var(--muted); font-size: 10px; text-align: center; }
|
|
2908
2961
|
.relationship-analysis-options { display: grid; gap: 14px; }
|
|
2909
2962
|
.relationship-character-field { min-width: 0; }
|
|
@@ -3683,6 +3736,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
3683
3736
|
.replace-scope-fieldset legend, .replace-scope-fieldset > p { grid-column: auto; }
|
|
3684
3737
|
.task-detail-overview { grid-template-columns: minmax(0, 1fr); padding: 12px; }
|
|
3685
3738
|
.task-detail-overview > * { grid-column: 1 !important; }
|
|
3739
|
+
.task-scope-panel-grid { grid-template-columns: minmax(0, 1fr); }
|
|
3740
|
+
.task-scope-available, .task-scope-selected { min-height: 0; }
|
|
3741
|
+
.task-scope-options, .task-scope-selected-list { max-height: 28vh; }
|
|
3686
3742
|
.task-result-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
3687
3743
|
.task-result-item-details, .task-result-json-loader { grid-template-columns: minmax(0, 1fr); }
|
|
3688
3744
|
.task-result-json-loader button { justify-self: start; }
|
package/dist/store.js
CHANGED
|
@@ -13,6 +13,7 @@ import { countWords, documentShortSearchTerms, escapeSqlLikePattern, id, json, n
|
|
|
13
13
|
import { buildWritingCalendar, writingDateKey } from "./writing-progress-time.js";
|
|
14
14
|
import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
|
|
15
15
|
const WORK_LIST_BATCH_SIZE = 500;
|
|
16
|
+
const ENTITY_LIST_BATCH_SIZE = 400;
|
|
16
17
|
export const RECYCLE_BIN_RETENTION_DAYS = 30;
|
|
17
18
|
function recycleBinExpiresAt(deletedAt) {
|
|
18
19
|
return new Date(new Date(deletedAt).getTime() + RECYCLE_BIN_RETENTION_DAYS * 24 * 60 * 60_000).toISOString();
|
|
@@ -338,6 +339,18 @@ export class Store {
|
|
|
338
339
|
const row = this.db.get("SELECT MAX(version_no) AS version_no FROM entity_versions WHERE entity_type = ? AND entity_id = ?", type, entityId);
|
|
339
340
|
return numberValue(row ?? {}, "version_no");
|
|
340
341
|
}
|
|
342
|
+
currentEntityVersionNos(type, entityIds) {
|
|
343
|
+
const versions = new Map();
|
|
344
|
+
for (let offset = 0; offset < entityIds.length; offset += ENTITY_LIST_BATCH_SIZE) {
|
|
345
|
+
const batchIds = entityIds.slice(offset, offset + ENTITY_LIST_BATCH_SIZE);
|
|
346
|
+
const placeholders = batchIds.map(() => "?").join(", ");
|
|
347
|
+
const rows = this.db.all(`SELECT entity_id, MAX(version_no) AS version_no FROM entity_versions
|
|
348
|
+
WHERE entity_type = ? AND entity_id IN (${placeholders}) GROUP BY entity_id`, type, ...batchIds);
|
|
349
|
+
for (const row of rows)
|
|
350
|
+
versions.set(requiredString(row, "entity_id"), numberValue(row, "version_no"));
|
|
351
|
+
}
|
|
352
|
+
return versions;
|
|
353
|
+
}
|
|
341
354
|
currentChapterVersionNo(chapterId) {
|
|
342
355
|
return numberValue(this.db.get("SELECT MAX(version_no) AS version_no FROM chapter_versions WHERE chapter_id = ?", chapterId) ?? {}, "version_no");
|
|
343
356
|
}
|
|
@@ -1263,18 +1276,19 @@ export class Store {
|
|
|
1263
1276
|
}));
|
|
1264
1277
|
return { ...work, volumes, directoryPage: pageResult };
|
|
1265
1278
|
}
|
|
1266
|
-
getStoryIndexChapterPage(workId, offset, limit) {
|
|
1279
|
+
getStoryIndexChapterPage(workId, offset, limit, options = {}) {
|
|
1267
1280
|
const work = this.getWork(workId);
|
|
1268
1281
|
const permissions = work.modulePermissions;
|
|
1269
1282
|
if (permissions.prose === "none")
|
|
1270
1283
|
return { totalChapters: 0, chapters: [] };
|
|
1284
|
+
const authorNoteFilter = options.excludeAuthorNotes ? " AND chapter.chapter_type <> '作者的话'" : "";
|
|
1271
1285
|
const countRow = this.db.get(`SELECT COUNT(*) AS count FROM chapters chapter
|
|
1272
1286
|
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
1273
|
-
WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL AND volume.deleted_at IS NULL`, workId);
|
|
1287
|
+
WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL AND volume.deleted_at IS NULL${authorNoteFilter}`, workId);
|
|
1274
1288
|
const chapterRows = this.db.all(`SELECT chapter.id, chapter.title, chapter.version_no, volume.title AS volume_title
|
|
1275
1289
|
FROM chapters chapter
|
|
1276
1290
|
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
1277
|
-
WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL AND volume.deleted_at IS NULL
|
|
1291
|
+
WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL AND volume.deleted_at IS NULL${authorNoteFilter}
|
|
1278
1292
|
ORDER BY volume.sort_order, volume.created_at, chapter.sort_order, chapter.created_at
|
|
1279
1293
|
LIMIT ? OFFSET ?`, workId, limit, offset);
|
|
1280
1294
|
const chapterIds = chapterRows.map((row) => requiredString(row, "id"));
|
|
@@ -2440,12 +2454,13 @@ export class Store {
|
|
|
2440
2454
|
this.db.run(`UPDATE chapter_paragraph_line_ranges SET chapter_version = ?
|
|
2441
2455
|
WHERE paragraph_id IN (SELECT id FROM chapter_paragraph_search WHERE chapter_id = ?)`, versionNo, chapterId);
|
|
2442
2456
|
}
|
|
2443
|
-
searchChapterParagraphs(workId, keyword, limit = 20) {
|
|
2457
|
+
searchChapterParagraphs(workId, keyword, limit = 20, options = {}) {
|
|
2444
2458
|
this.getWork(workId);
|
|
2445
2459
|
const normalizedKeyword = normalizeDocumentSearchText(keyword.trim());
|
|
2446
2460
|
if (!normalizedKeyword)
|
|
2447
2461
|
return [];
|
|
2448
2462
|
const safeLimit = Math.min(100, Math.max(1, Math.trunc(limit)));
|
|
2463
|
+
const authorNoteFilter = options.excludeAuthorNotes ? " AND chapter.chapter_type <> '作者的话'" : "";
|
|
2449
2464
|
const columns = `SELECT paragraph.chapter_id, chapter.title AS chapter_title, paragraph.content
|
|
2450
2465
|
FROM chapter_paragraph_search paragraph
|
|
2451
2466
|
JOIN chapters chapter ON chapter.id = paragraph.chapter_id
|
|
@@ -2453,12 +2468,12 @@ export class Store {
|
|
|
2453
2468
|
const rows = [...normalizedKeyword].length < 3
|
|
2454
2469
|
? this.db.all(`${columns}
|
|
2455
2470
|
JOIN chapter_paragraph_short_terms term ON term.paragraph_id = paragraph.id
|
|
2456
|
-
WHERE paragraph.work_id = ? AND chapter.deleted_at IS NULL AND term.term = ?
|
|
2471
|
+
WHERE paragraph.work_id = ? AND chapter.deleted_at IS NULL${authorNoteFilter} AND term.term = ?
|
|
2457
2472
|
ORDER BY volume.sort_order, chapter.sort_order, paragraph.paragraph_order
|
|
2458
2473
|
LIMIT ?`, workId, normalizedKeyword, safeLimit)
|
|
2459
2474
|
: this.db.all(`${columns}
|
|
2460
2475
|
JOIN chapter_paragraph_search_fts fts ON fts.rowid = paragraph.id
|
|
2461
|
-
WHERE paragraph.work_id = ? AND chapter.deleted_at IS NULL AND chapter_paragraph_search_fts MATCH ?
|
|
2476
|
+
WHERE paragraph.work_id = ? AND chapter.deleted_at IS NULL${authorNoteFilter} AND chapter_paragraph_search_fts MATCH ?
|
|
2462
2477
|
ORDER BY volume.sort_order, chapter.sort_order, paragraph.paragraph_order
|
|
2463
2478
|
LIMIT ?`, workId, `"${normalizedKeyword.replaceAll('"', '""')}"`, safeLimit);
|
|
2464
2479
|
return rows.map((row) => ({
|
|
@@ -3029,20 +3044,25 @@ export class Store {
|
|
|
3029
3044
|
WHERE fo.foreshadow_id = ? ORDER BY v.sort_order, c.sort_order, fo.created_at`, foreshadowId).map((item) => this.mapForeshadowOccurrence(item));
|
|
3030
3045
|
const status = requiredString(row, "status");
|
|
3031
3046
|
const plannedPayoffChapterId = optionalString(row, "planned_payoff_chapter_id");
|
|
3047
|
+
const overdue = Boolean(currentChapterId && plannedPayoffChapterId && ["planned", "planted"].includes(status)
|
|
3048
|
+
&& this.chapterSequence(workId, plannedPayoffChapterId) < this.chapterSequence(workId, currentChapterId));
|
|
3049
|
+
return this.mapForeshadow(row, occurrences, this.currentEntityVersionNo("foreshadow", foreshadowId), overdue);
|
|
3050
|
+
}
|
|
3051
|
+
mapForeshadow(row, occurrences, versionNo, overdue) {
|
|
3052
|
+
const status = requiredString(row, "status");
|
|
3032
3053
|
return {
|
|
3033
3054
|
id: requiredString(row, "id"),
|
|
3034
|
-
workId,
|
|
3055
|
+
workId: requiredString(row, "work_id"),
|
|
3035
3056
|
title: requiredString(row, "title"),
|
|
3036
3057
|
description: requiredString(row, "description"),
|
|
3037
3058
|
status,
|
|
3038
3059
|
importance: requiredString(row, "importance"),
|
|
3039
|
-
plannedPayoffChapterId,
|
|
3060
|
+
plannedPayoffChapterId: optionalString(row, "planned_payoff_chapter_id"),
|
|
3040
3061
|
resolutionNote: requiredString(row, "resolution_note"),
|
|
3041
3062
|
unresolved: status === "planned" || status === "planted",
|
|
3042
|
-
overdue
|
|
3043
|
-
&& this.chapterSequence(workId, plannedPayoffChapterId) < this.chapterSequence(workId, currentChapterId)),
|
|
3063
|
+
overdue,
|
|
3044
3064
|
occurrences,
|
|
3045
|
-
versionNo
|
|
3065
|
+
versionNo,
|
|
3046
3066
|
createdAt: requiredString(row, "created_at"),
|
|
3047
3067
|
updatedAt: requiredString(row, "updated_at")
|
|
3048
3068
|
};
|
|
@@ -3054,8 +3074,9 @@ export class Store {
|
|
|
3054
3074
|
const where = status === "unresolved"
|
|
3055
3075
|
? "AND status IN ('planned', 'planted')"
|
|
3056
3076
|
: status === "resolved" ? "AND status IN ('resolved', 'abandoned')" : "";
|
|
3057
|
-
|
|
3058
|
-
ORDER BY CASE importance WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, created_at`, workId)
|
|
3077
|
+
const rows = this.db.all(`SELECT * FROM foreshadows WHERE work_id = ? ${where}
|
|
3078
|
+
ORDER BY CASE importance WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, created_at`, workId);
|
|
3079
|
+
return this.mapForeshadowList(rows, currentChapterId);
|
|
3059
3080
|
}
|
|
3060
3081
|
listForeshadowsPage(workId, pagination, status = "all", currentChapterId) {
|
|
3061
3082
|
this.getWork(workId);
|
|
@@ -3065,9 +3086,63 @@ export class Store {
|
|
|
3065
3086
|
? "AND status IN ('planned', 'planted')"
|
|
3066
3087
|
: status === "resolved" ? "AND status IN ('resolved', 'abandoned')" : "";
|
|
3067
3088
|
const page = paginationSql(pagination);
|
|
3068
|
-
const rows = this.db.all(`SELECT
|
|
3089
|
+
const rows = this.db.all(`SELECT * FROM foreshadows WHERE work_id = ? ${where}
|
|
3069
3090
|
ORDER BY CASE importance WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, created_at${page.sql}`, workId, ...page.params);
|
|
3070
|
-
return paginated(
|
|
3091
|
+
return paginated(this.mapForeshadowList(rows, currentChapterId), pagination);
|
|
3092
|
+
}
|
|
3093
|
+
mapForeshadowList(rows, currentChapterId) {
|
|
3094
|
+
if (rows.length === 0)
|
|
3095
|
+
return [];
|
|
3096
|
+
const foreshadowIds = rows.map((row) => requiredString(row, "id"));
|
|
3097
|
+
const batch = {
|
|
3098
|
+
occurrences: new Map(),
|
|
3099
|
+
versions: this.currentEntityVersionNos("foreshadow", foreshadowIds),
|
|
3100
|
+
chapterSequences: new Map()
|
|
3101
|
+
};
|
|
3102
|
+
for (let offset = 0; offset < foreshadowIds.length; offset += ENTITY_LIST_BATCH_SIZE) {
|
|
3103
|
+
const batchIds = foreshadowIds.slice(offset, offset + ENTITY_LIST_BATCH_SIZE);
|
|
3104
|
+
const placeholders = batchIds.map(() => "?").join(", ");
|
|
3105
|
+
const occurrences = this.db.all(`SELECT fo.*, c.title AS chapter_title, c.volume_id, c.sort_order AS chapter_order,
|
|
3106
|
+
v.title AS volume_title, v.sort_order AS volume_order
|
|
3107
|
+
FROM foreshadow_occurrences fo
|
|
3108
|
+
JOIN chapters c ON c.id = fo.chapter_id
|
|
3109
|
+
JOIN volumes v ON v.id = c.volume_id
|
|
3110
|
+
WHERE fo.foreshadow_id IN (${placeholders})
|
|
3111
|
+
ORDER BY fo.foreshadow_id, v.sort_order, c.sort_order, fo.created_at`, ...batchIds);
|
|
3112
|
+
for (const occurrence of occurrences) {
|
|
3113
|
+
const foreshadowId = requiredString(occurrence, "foreshadow_id");
|
|
3114
|
+
const grouped = batch.occurrences.get(foreshadowId) ?? [];
|
|
3115
|
+
grouped.push(this.mapForeshadowOccurrence(occurrence));
|
|
3116
|
+
batch.occurrences.set(foreshadowId, grouped);
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
if (currentChapterId) {
|
|
3120
|
+
const chapterIds = [...new Set([
|
|
3121
|
+
currentChapterId,
|
|
3122
|
+
...rows.map((row) => optionalString(row, "planned_payoff_chapter_id")).filter((chapterId) => Boolean(chapterId))
|
|
3123
|
+
])];
|
|
3124
|
+
for (let offset = 0; offset < chapterIds.length; offset += ENTITY_LIST_BATCH_SIZE) {
|
|
3125
|
+
const batchIds = chapterIds.slice(offset, offset + ENTITY_LIST_BATCH_SIZE);
|
|
3126
|
+
const placeholders = batchIds.map(() => "?").join(", ");
|
|
3127
|
+
const sequences = this.db.all(`SELECT c.id, v.sort_order * 1000000 + c.sort_order AS sequence
|
|
3128
|
+
FROM chapters c JOIN volumes v ON v.id = c.volume_id
|
|
3129
|
+
WHERE c.id IN (${placeholders}) AND c.work_id = ?`, ...batchIds, requiredString(rows[0] ?? {}, "work_id"));
|
|
3130
|
+
for (const sequence of sequences) {
|
|
3131
|
+
batch.chapterSequences.set(requiredString(sequence, "id"), numberValue(sequence, "sequence"));
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
const currentChapterSequence = currentChapterId
|
|
3136
|
+
? batch.chapterSequences.get(currentChapterId) ?? Number.MAX_SAFE_INTEGER
|
|
3137
|
+
: Number.MAX_SAFE_INTEGER;
|
|
3138
|
+
return rows.map((row) => {
|
|
3139
|
+
const foreshadowId = requiredString(row, "id");
|
|
3140
|
+
const status = requiredString(row, "status");
|
|
3141
|
+
const plannedPayoffChapterId = optionalString(row, "planned_payoff_chapter_id");
|
|
3142
|
+
const overdue = Boolean(currentChapterId && plannedPayoffChapterId && ["planned", "planted"].includes(status)
|
|
3143
|
+
&& (batch.chapterSequences.get(plannedPayoffChapterId) ?? Number.MAX_SAFE_INTEGER) < currentChapterSequence);
|
|
3144
|
+
return this.mapForeshadow(row, batch.occurrences.get(foreshadowId) ?? [], batch.versions.get(foreshadowId) ?? 0, overdue);
|
|
3145
|
+
});
|
|
3071
3146
|
}
|
|
3072
3147
|
listChapterForeshadowReminders(workId, chapterId) {
|
|
3073
3148
|
this.getWork(workId);
|
|
@@ -3750,13 +3825,16 @@ export class Store {
|
|
|
3750
3825
|
}
|
|
3751
3826
|
listOrganizations(workId, includeMarkdown = true) {
|
|
3752
3827
|
this.getWork(workId);
|
|
3753
|
-
|
|
3828
|
+
const rows = this.db.all("SELECT * FROM organizations WHERE work_id = ? ORDER BY name", workId);
|
|
3829
|
+
const batch = this.organizationListBatch(rows);
|
|
3830
|
+
return rows.map((row) => this.mapOrganization(row, includeMarkdown, batch));
|
|
3754
3831
|
}
|
|
3755
3832
|
listOrganizationsPage(workId, pagination, includeMarkdown = true) {
|
|
3756
3833
|
this.getWork(workId);
|
|
3757
3834
|
const page = paginationSql(pagination);
|
|
3758
3835
|
const rows = this.db.all(`SELECT * FROM organizations WHERE work_id = ? ORDER BY name${page.sql}`, workId, ...page.params);
|
|
3759
|
-
|
|
3836
|
+
const batch = this.organizationListBatch(rows);
|
|
3837
|
+
return paginated(rows.map((row) => this.mapOrganization(row, includeMarkdown, batch)), pagination);
|
|
3760
3838
|
}
|
|
3761
3839
|
getOrganization(organizationId) {
|
|
3762
3840
|
const row = this.db.get("SELECT * FROM organizations WHERE id = ?", organizationId);
|
|
@@ -3843,20 +3921,50 @@ export class Store {
|
|
|
3843
3921
|
});
|
|
3844
3922
|
return { mergeId, target: this.getOrganization(targetOrganizationId), source };
|
|
3845
3923
|
}
|
|
3846
|
-
|
|
3924
|
+
organizationListBatch(rows) {
|
|
3925
|
+
const organizationIds = rows.map((row) => requiredString(row, "id"));
|
|
3926
|
+
const batch = {
|
|
3927
|
+
members: new Map(),
|
|
3928
|
+
versions: this.currentEntityVersionNos("organization", organizationIds)
|
|
3929
|
+
};
|
|
3930
|
+
for (let offset = 0; offset < organizationIds.length; offset += ENTITY_LIST_BATCH_SIZE) {
|
|
3931
|
+
const batchIds = organizationIds.slice(offset, offset + ENTITY_LIST_BATCH_SIZE);
|
|
3932
|
+
const placeholders = batchIds.map(() => "?").join(", ");
|
|
3933
|
+
const members = this.db.all(`SELECT m.organization_id, c.id, c.name, m.role, m.note
|
|
3934
|
+
FROM character_organization_memberships m
|
|
3935
|
+
JOIN characters c ON c.id = m.character_id
|
|
3936
|
+
WHERE m.organization_id IN (${placeholders}) ORDER BY m.organization_id, c.name`, ...batchIds);
|
|
3937
|
+
for (const member of members) {
|
|
3938
|
+
const organizationId = requiredString(member, "organization_id");
|
|
3939
|
+
const grouped = batch.members.get(organizationId) ?? [];
|
|
3940
|
+
grouped.push({
|
|
3941
|
+
characterId: requiredString(member, "id"),
|
|
3942
|
+
name: requiredString(member, "name"),
|
|
3943
|
+
role: requiredString(member, "role"),
|
|
3944
|
+
note: requiredString(member, "note")
|
|
3945
|
+
});
|
|
3946
|
+
batch.members.set(organizationId, grouped);
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
return batch;
|
|
3950
|
+
}
|
|
3951
|
+
mapOrganization(row, includeMarkdown = true, batch) {
|
|
3952
|
+
const organizationId = requiredString(row, "id");
|
|
3847
3953
|
const settingsSections = knowledgeSectionsFromStored(row.settings_sections_json, json(requiredString(row, "settings_json"), []));
|
|
3848
3954
|
const settings = settingsFromKnowledgeSections(settingsSections);
|
|
3849
|
-
const members =
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3955
|
+
const members = batch
|
|
3956
|
+
? batch.members.get(organizationId) ?? []
|
|
3957
|
+
: this.db.all(`SELECT c.id, c.name, m.role, m.note
|
|
3958
|
+
FROM character_organization_memberships m
|
|
3959
|
+
JOIN characters c ON c.id = m.character_id
|
|
3960
|
+
WHERE m.organization_id = ? ORDER BY c.name`, organizationId).map((member) => ({
|
|
3961
|
+
characterId: requiredString(member, "id"),
|
|
3962
|
+
name: requiredString(member, "name"),
|
|
3963
|
+
role: requiredString(member, "role"),
|
|
3964
|
+
note: requiredString(member, "note")
|
|
3965
|
+
}));
|
|
3858
3966
|
return {
|
|
3859
|
-
id:
|
|
3967
|
+
id: organizationId,
|
|
3860
3968
|
workId: requiredString(row, "work_id"),
|
|
3861
3969
|
name: requiredString(row, "name"),
|
|
3862
3970
|
description: requiredString(row, "description"),
|
|
@@ -3866,7 +3974,7 @@ export class Store {
|
|
|
3866
3974
|
: { settings: [], settingsCount: settingsSections.length }),
|
|
3867
3975
|
memberIds: members.map((member) => member.characterId),
|
|
3868
3976
|
members,
|
|
3869
|
-
versionNo: this.currentEntityVersionNo("organization",
|
|
3977
|
+
versionNo: batch ? batch.versions.get(organizationId) ?? 0 : this.currentEntityVersionNo("organization", organizationId),
|
|
3870
3978
|
createdAt: requiredString(row, "created_at"),
|
|
3871
3979
|
updatedAt: requiredString(row, "updated_at")
|
|
3872
3980
|
};
|
|
@@ -5652,6 +5760,43 @@ export class Store {
|
|
|
5652
5760
|
throw notFound("AI 对话消息");
|
|
5653
5761
|
return this.mapAiConversationMessage(persistInterruption(message));
|
|
5654
5762
|
}
|
|
5763
|
+
upsertAiConversationAssistantMessage(conversationId, requestId, content, metadata = {}, syncSearchIndex = false) {
|
|
5764
|
+
const normalizedRequestId = requestId.trim();
|
|
5765
|
+
if (!normalizedRequestId)
|
|
5766
|
+
throw new AppError(400, "AI_MESSAGE_REQUEST_ID_REQUIRED", "AI 助手消息缺少请求标识");
|
|
5767
|
+
const existing = this.db.get("SELECT * FROM ai_conversation_messages WHERE conversation_id = ? AND request_id = ?", conversationId, normalizedRequestId);
|
|
5768
|
+
if (!existing) {
|
|
5769
|
+
return this.addAiConversationMessage(conversationId, {
|
|
5770
|
+
role: "assistant",
|
|
5771
|
+
content,
|
|
5772
|
+
requestId: normalizedRequestId,
|
|
5773
|
+
metadata
|
|
5774
|
+
});
|
|
5775
|
+
}
|
|
5776
|
+
if (requiredString(existing, "role") !== "assistant") {
|
|
5777
|
+
throw new AppError(409, "AI_MESSAGE_ROLE_MISMATCH", "请求标识已用于用户消息");
|
|
5778
|
+
}
|
|
5779
|
+
const currentMetadata = json(requiredString(existing, "metadata_json"), {});
|
|
5780
|
+
const nextMetadata = { ...currentMetadata, ...metadata };
|
|
5781
|
+
const contentChanged = requiredString(existing, "content") !== content;
|
|
5782
|
+
const metadataChanged = JSON.stringify(currentMetadata) !== JSON.stringify(nextMetadata);
|
|
5783
|
+
if (!contentChanged && !metadataChanged) {
|
|
5784
|
+
if (syncSearchIndex)
|
|
5785
|
+
this.syncAiHistorySearchShortTermsForSource("message", requiredString(existing, "id"));
|
|
5786
|
+
return this.mapAiConversationMessage(existing);
|
|
5787
|
+
}
|
|
5788
|
+
const timestamp = now();
|
|
5789
|
+
this.db.transaction(() => {
|
|
5790
|
+
this.db.run("UPDATE ai_conversation_messages SET content = ?, metadata_json = ? WHERE id = ?", content, JSON.stringify(nextMetadata), requiredString(existing, "id"));
|
|
5791
|
+
this.db.run("UPDATE ai_conversations SET updated_at = ? WHERE id = ?", timestamp, conversationId);
|
|
5792
|
+
if (syncSearchIndex)
|
|
5793
|
+
this.syncAiHistorySearchShortTermsForSource("message", requiredString(existing, "id"));
|
|
5794
|
+
});
|
|
5795
|
+
const updated = this.db.get("SELECT * FROM ai_conversation_messages WHERE id = ?", requiredString(existing, "id"));
|
|
5796
|
+
if (!updated)
|
|
5797
|
+
throw notFound("AI 对话消息");
|
|
5798
|
+
return this.mapAiConversationMessage(updated);
|
|
5799
|
+
}
|
|
5655
5800
|
beginAiConversationStreamRequest(input, referenceTime = new Date()) {
|
|
5656
5801
|
const timestamp = referenceTime.toISOString();
|
|
5657
5802
|
const leaseExpiresAt = new Date(referenceTime.getTime() + AI_CONVERSATION_STREAM_REQUEST_LEASE_MS).toISOString();
|
|
@@ -5769,10 +5914,18 @@ export class Store {
|
|
|
5769
5914
|
if (!assistant)
|
|
5770
5915
|
throw new AppError(400, "AI_STREAM_ASSISTANT_MISMATCH", "AI 回复消息不属于当前对话请求");
|
|
5771
5916
|
}
|
|
5917
|
+
const resolvedAssistantMessageId = assistantMessageId ?? (() => {
|
|
5918
|
+
const userMessageId = optionalString(request, "user_message_id");
|
|
5919
|
+
if (!userMessageId)
|
|
5920
|
+
return null;
|
|
5921
|
+
const assistant = this.db.get(`SELECT id FROM ai_conversation_messages
|
|
5922
|
+
WHERE conversation_id = ? AND role = 'assistant' AND request_id = ?`, requiredString(request, "conversation_id"), `assistant:${userMessageId}`);
|
|
5923
|
+
return assistant ? requiredString(assistant, "id") : null;
|
|
5924
|
+
})();
|
|
5772
5925
|
this.db.run(`UPDATE ai_conversation_stream_requests
|
|
5773
5926
|
SET status = ?, terminal_reason = ?, assistant_message_id = COALESCE(assistant_message_id, ?),
|
|
5774
5927
|
lease_expires_at = NULL, updated_at = ?, completed_at = ?
|
|
5775
|
-
WHERE id = ? AND status = 'in_progress'`, status, terminalReason.slice(0, 500),
|
|
5928
|
+
WHERE id = ? AND status = 'in_progress'`, status, terminalReason.slice(0, 500), resolvedAssistantMessageId, timestamp, timestamp, requestId);
|
|
5776
5929
|
const completed = this.db.get("SELECT * FROM ai_conversation_stream_requests WHERE id = ?", requestId);
|
|
5777
5930
|
if (!completed)
|
|
5778
5931
|
throw notFound("AI 对话请求");
|
|
@@ -6023,11 +6176,13 @@ export class Store {
|
|
|
6023
6176
|
...(Array.isArray(scope.chapterIds) ? scope.chapterIds.filter((value) => typeof value === "string") : [])
|
|
6024
6177
|
];
|
|
6025
6178
|
for (const chapterId of [...new Set(selectedChapterIds)]) {
|
|
6026
|
-
const chapter = this.db.get("SELECT work_id, version_no FROM chapters WHERE id = ? AND deleted_at IS NULL", chapterId);
|
|
6179
|
+
const chapter = this.db.get("SELECT work_id, version_no, chapter_type FROM chapters WHERE id = ? AND deleted_at IS NULL", chapterId);
|
|
6027
6180
|
if (!chapter)
|
|
6028
6181
|
throw notFound("章节");
|
|
6029
6182
|
if (requiredString(chapter, "work_id") !== workId)
|
|
6030
6183
|
throw new AppError(400, "CHAPTER_WORK_MISMATCH", "章节不属于当前作品");
|
|
6184
|
+
if (requiredString(chapter, "chapter_type") === "作者的话")
|
|
6185
|
+
continue;
|
|
6031
6186
|
sourceVersions[chapterId] = numberValue(chapter, "version_no");
|
|
6032
6187
|
}
|
|
6033
6188
|
if (scope.type === "book" || scope.type === "volume") {
|
|
@@ -6051,6 +6206,7 @@ export class Store {
|
|
|
6051
6206
|
FROM chapters chapter
|
|
6052
6207
|
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
6053
6208
|
WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL AND volume.deleted_at IS NULL
|
|
6209
|
+
AND chapter.chapter_type <> '作者的话'
|
|
6054
6210
|
${volumeFilter}`, workId, ...(scope.type === "volume" ? [...selectedVolumeIdSet] : []));
|
|
6055
6211
|
for (const chapter of chapterRows) {
|
|
6056
6212
|
sourceVersions[requiredString(chapter, "id")] = numberValue(chapter, "version_no");
|