@musnows/scriverse 1.0.12 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +3 -0
- package/README.md +3 -0
- package/dist/ai-stream-steer.js +96 -0
- package/dist/ai-stream-steer.js.map +1 -0
- package/dist/ai-write-plans.js +44 -38
- package/dist/ai-write-plans.js.map +1 -1
- package/dist/ai.js +241 -128
- package/dist/ai.js.map +1 -1
- package/dist/app.js +29 -5
- package/dist/app.js.map +1 -1
- package/dist/attachment-storage.js +3 -0
- package/dist/attachment-storage.js.map +1 -1
- package/dist/chapter-annotation-note.js +14 -0
- package/dist/chapter-annotation-note.js.map +1 -0
- package/dist/character-structured-fields.js +7 -0
- package/dist/character-structured-fields.js.map +1 -0
- package/dist/cli-contract.js +1 -1
- package/dist/cli-contract.js.map +1 -1
- package/dist/im.js +2 -1
- package/dist/im.js.map +1 -1
- package/dist/public/ai-prompt-keyboard.js +5 -1
- package/dist/public/ai-prompt-queue.js +226 -0
- package/dist/public/ai-send-mode.js +31 -0
- package/dist/public/app.js +564 -37
- package/dist/public/character-profile.d.ts +7 -0
- package/dist/public/character-profile.js +123 -5
- package/dist/public/index.html +11 -4
- package/dist/public/styles.css +28 -1
- package/dist/server-runtime.js +2 -0
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +7 -6
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
export const AI_PROMPT_QUEUE_LIMIT = 20;
|
|
2
|
+
export const AI_PROMPT_QUEUE_DRAG_PREFIX = "scriverse-ai-prompt-queue:";
|
|
3
|
+
|
|
4
|
+
function normalizedId(value) {
|
|
5
|
+
const normalized = String(value ?? "").trim();
|
|
6
|
+
return normalized || null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function nextQueueItemId() {
|
|
10
|
+
return globalThis.crypto?.randomUUID?.() ?? `queue-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function snapshotComposer(input = {}) {
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
text: String(input.text ?? "").trim(),
|
|
16
|
+
markup: String(input.markup ?? input.text ?? "").trim(),
|
|
17
|
+
citations: Object.freeze([...(Array.isArray(input.citations) ? input.citations : [])].map((citation) => ({ ...citation }))),
|
|
18
|
+
references: Object.freeze([...(Array.isArray(input.references) ? input.references : [])].map((reference) => ({ ...reference }))),
|
|
19
|
+
images: Object.freeze([...(Array.isArray(input.images) ? input.images : [])].map((image) => ({ ...image }))),
|
|
20
|
+
sceneDirection: String(input.sceneDirection ?? "").trim(),
|
|
21
|
+
scenePin: input.scenePin && typeof input.scenePin === "object" ? Object.freeze({ ...input.scenePin }) : null
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function queueEmptyError() {
|
|
26
|
+
const error = new Error("排队 Prompt 不能为空");
|
|
27
|
+
error.code = "AI_PROMPT_QUEUE_EMPTY";
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function queueLimitError() {
|
|
32
|
+
const error = new Error(`最多排队 ${AI_PROMPT_QUEUE_LIMIT} 条 Prompt`);
|
|
33
|
+
error.code = "AI_PROMPT_QUEUE_LIMIT";
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sameQueuedOrder(left, right) {
|
|
38
|
+
return left.length === right.length && left.every((item, index) => item.id === right[index]?.id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function aiPromptQueuePreview(text, limit = 72) {
|
|
42
|
+
const normalized = String(text ?? "").replace(/\s+/gu, " ").trim();
|
|
43
|
+
if (!normalized) return "空 Prompt";
|
|
44
|
+
const max = Number.isSafeInteger(limit) && limit > 0 ? limit : 72;
|
|
45
|
+
return normalized.length > max ? `${normalized.slice(0, max - 1)}…` : normalized;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function queuedPromptSteerContent(item) {
|
|
49
|
+
return [item?.sceneDirection, item?.text]
|
|
50
|
+
.map((value) => String(value ?? "").trim())
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.join("\n\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function queuedPromptEditContent(item) {
|
|
56
|
+
if (String(item?.text ?? "").trim()) return String(item.text ?? "");
|
|
57
|
+
return String(item?.sceneDirection ?? "");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function queuedPromptEditPatch(item, nextText) {
|
|
61
|
+
const value = String(nextText ?? "");
|
|
62
|
+
if (String(item?.text ?? "").trim()) {
|
|
63
|
+
return { text: value, markup: value };
|
|
64
|
+
}
|
|
65
|
+
return { sceneDirection: value };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function canSendQueuedPromptAsSteer(item, streaming) {
|
|
69
|
+
if (!streaming || !item) return false;
|
|
70
|
+
return Boolean(queuedPromptSteerContent(item));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function aiPromptQueueDragPayload(itemId) {
|
|
74
|
+
const id = normalizedId(itemId);
|
|
75
|
+
return id ? `${AI_PROMPT_QUEUE_DRAG_PREFIX}${id}` : "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseAiPromptQueueDragPayload(value) {
|
|
79
|
+
const text = String(value ?? "");
|
|
80
|
+
if (!text.startsWith(AI_PROMPT_QUEUE_DRAG_PREFIX)) return null;
|
|
81
|
+
return normalizedId(text.slice(AI_PROMPT_QUEUE_DRAG_PREFIX.length));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function moveQueuedPromptItem(items, sourceId, targetId, placeAfter = false) {
|
|
85
|
+
const current = Array.isArray(items) ? items : [];
|
|
86
|
+
const source = normalizedId(sourceId);
|
|
87
|
+
const target = normalizedId(targetId);
|
|
88
|
+
if (!source || !target || source === target) return current;
|
|
89
|
+
const from = current.findIndex((item) => item.id === source);
|
|
90
|
+
if (from < 0) return current;
|
|
91
|
+
const next = [...current];
|
|
92
|
+
const [moved] = next.splice(from, 1);
|
|
93
|
+
let to = next.findIndex((item) => item.id === target);
|
|
94
|
+
if (to < 0) return current;
|
|
95
|
+
if (placeAfter) to += 1;
|
|
96
|
+
next.splice(to, 0, moved);
|
|
97
|
+
return sameQueuedOrder(current, next) ? current : next;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function moveQueuedPromptItemByOffset(items, itemId, offset) {
|
|
101
|
+
const current = Array.isArray(items) ? items : [];
|
|
102
|
+
const id = normalizedId(itemId);
|
|
103
|
+
if (!id || !Number.isSafeInteger(offset) || offset === 0) return current;
|
|
104
|
+
const from = current.findIndex((item) => item.id === id);
|
|
105
|
+
const to = from + offset;
|
|
106
|
+
if (from < 0 || to < 0 || to >= current.length) return current;
|
|
107
|
+
const next = [...current];
|
|
108
|
+
const [moved] = next.splice(from, 1);
|
|
109
|
+
next.splice(to, 0, moved);
|
|
110
|
+
return next;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createAiPromptQueue() {
|
|
114
|
+
const queues = new Map();
|
|
115
|
+
|
|
116
|
+
const list = (tabId) => [...(queues.get(normalizedId(tabId) ?? "default") ?? [])];
|
|
117
|
+
|
|
118
|
+
const replace = (key, next) => {
|
|
119
|
+
if (next.length) queues.set(key, Object.freeze(next));
|
|
120
|
+
else queues.delete(key);
|
|
121
|
+
return list(key);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const enqueue = (tabId, composer) => {
|
|
125
|
+
const key = normalizedId(tabId) ?? "default";
|
|
126
|
+
const snapshot = snapshotComposer(composer);
|
|
127
|
+
if (!snapshot.text && !snapshot.sceneDirection) throw queueEmptyError();
|
|
128
|
+
const current = list(key);
|
|
129
|
+
if (current.length >= AI_PROMPT_QUEUE_LIMIT) throw queueLimitError();
|
|
130
|
+
const item = Object.freeze({
|
|
131
|
+
id: nextQueueItemId(),
|
|
132
|
+
createdAt: new Date().toISOString(),
|
|
133
|
+
...snapshot
|
|
134
|
+
});
|
|
135
|
+
queues.set(key, Object.freeze([...current, item]));
|
|
136
|
+
return item;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const remove = (tabId, itemId) => {
|
|
140
|
+
const key = normalizedId(tabId) ?? "default";
|
|
141
|
+
const id = normalizedId(itemId);
|
|
142
|
+
const current = list(key);
|
|
143
|
+
const next = current.filter((item) => item.id !== id);
|
|
144
|
+
if (next.length === current.length) return null;
|
|
145
|
+
replace(key, next);
|
|
146
|
+
return current.find((item) => item.id === id) ?? null;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const takeNext = (tabId) => {
|
|
150
|
+
const key = normalizedId(tabId) ?? "default";
|
|
151
|
+
const [first, ...rest] = list(key);
|
|
152
|
+
if (!first) return null;
|
|
153
|
+
replace(key, rest);
|
|
154
|
+
return first;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const restore = (tabId, item, index = 0) => {
|
|
158
|
+
const key = normalizedId(tabId) ?? "default";
|
|
159
|
+
if (!item?.id) return null;
|
|
160
|
+
const current = list(key).filter((entry) => entry.id !== item.id);
|
|
161
|
+
if (current.length >= AI_PROMPT_QUEUE_LIMIT) throw queueLimitError();
|
|
162
|
+
const next = [...current];
|
|
163
|
+
const at = Math.max(0, Math.min(Number.isSafeInteger(index) ? index : 0, next.length));
|
|
164
|
+
next.splice(at, 0, item);
|
|
165
|
+
queues.set(key, Object.freeze(next));
|
|
166
|
+
return item;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const update = (tabId, itemId, patch = {}) => {
|
|
170
|
+
const key = normalizedId(tabId) ?? "default";
|
|
171
|
+
const id = normalizedId(itemId);
|
|
172
|
+
const current = list(key);
|
|
173
|
+
const index = current.findIndex((item) => item.id === id);
|
|
174
|
+
if (index < 0) return null;
|
|
175
|
+
const existing = current[index];
|
|
176
|
+
const snapshot = snapshotComposer({ ...existing, ...patch });
|
|
177
|
+
if (!snapshot.text && !snapshot.sceneDirection) throw queueEmptyError();
|
|
178
|
+
const nextItem = Object.freeze({
|
|
179
|
+
id: existing.id,
|
|
180
|
+
createdAt: existing.createdAt,
|
|
181
|
+
...snapshot
|
|
182
|
+
});
|
|
183
|
+
const next = [...current];
|
|
184
|
+
next[index] = nextItem;
|
|
185
|
+
queues.set(key, Object.freeze(next));
|
|
186
|
+
return nextItem;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const move = (tabId, sourceId, targetId, placeAfter = false) => {
|
|
190
|
+
const key = normalizedId(tabId) ?? "default";
|
|
191
|
+
const current = list(key);
|
|
192
|
+
const next = moveQueuedPromptItem(current, sourceId, targetId, placeAfter);
|
|
193
|
+
if (next === current) return current;
|
|
194
|
+
queues.set(key, Object.freeze(next));
|
|
195
|
+
return list(key);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const moveByOffset = (tabId, itemId, offset) => {
|
|
199
|
+
const key = normalizedId(tabId) ?? "default";
|
|
200
|
+
const current = list(key);
|
|
201
|
+
const next = moveQueuedPromptItemByOffset(current, itemId, offset);
|
|
202
|
+
if (next === current) return current;
|
|
203
|
+
queues.set(key, Object.freeze(next));
|
|
204
|
+
return list(key);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const clear = (tabId) => {
|
|
208
|
+
const key = normalizedId(tabId) ?? "default";
|
|
209
|
+
const current = list(key);
|
|
210
|
+
queues.delete(key);
|
|
211
|
+
return current;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
clear,
|
|
216
|
+
count: (tabId) => list(tabId).length,
|
|
217
|
+
enqueue,
|
|
218
|
+
list,
|
|
219
|
+
move,
|
|
220
|
+
moveByOffset,
|
|
221
|
+
remove,
|
|
222
|
+
restore,
|
|
223
|
+
takeNext,
|
|
224
|
+
update
|
|
225
|
+
};
|
|
226
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const AI_SEND_MODE_QUEUE = "queue";
|
|
2
|
+
export const AI_SEND_MODE_STEER = "steer";
|
|
3
|
+
export const AI_SEND_MODE_STORAGE_KEY = "scriverse.ai-send-mode";
|
|
4
|
+
|
|
5
|
+
export function normalizeAiSendMode(value) {
|
|
6
|
+
return String(value ?? "").trim() === AI_SEND_MODE_STEER ? AI_SEND_MODE_STEER : AI_SEND_MODE_QUEUE;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function readStoredAiSendMode(storage) {
|
|
10
|
+
try {
|
|
11
|
+
return normalizeAiSendMode(storage?.getItem?.(AI_SEND_MODE_STORAGE_KEY));
|
|
12
|
+
} catch {
|
|
13
|
+
return AI_SEND_MODE_QUEUE;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function writeStoredAiSendMode(storage, value) {
|
|
18
|
+
const normalized = normalizeAiSendMode(value);
|
|
19
|
+
try {
|
|
20
|
+
storage?.setItem?.(AI_SEND_MODE_STORAGE_KEY, normalized);
|
|
21
|
+
} catch {
|
|
22
|
+
/* 浏览器禁用存储时仅保留本次选择 */
|
|
23
|
+
}
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function aiSendModeAction(mode, streaming, hasComposerContent = true) {
|
|
28
|
+
if (!streaming) return "send";
|
|
29
|
+
if (!hasComposerContent) return "stop";
|
|
30
|
+
return normalizeAiSendMode(mode) === AI_SEND_MODE_STEER ? "steer" : "queue";
|
|
31
|
+
}
|