@musnows/scriverse 0.7.2 → 0.7.4

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.
Files changed (82) hide show
  1. package/README.en.md +3 -0
  2. package/README.md +8 -0
  3. package/dist/ai-connectivity-test.js +109 -0
  4. package/dist/ai-connectivity-test.js.map +1 -0
  5. package/dist/ai-conversation-export.js +70 -0
  6. package/dist/ai-conversation-export.js.map +1 -0
  7. package/dist/ai-stream-timeout.js +18 -0
  8. package/dist/ai-stream-timeout.js.map +1 -0
  9. package/dist/ai.js +926 -354
  10. package/dist/ai.js.map +1 -1
  11. package/dist/app.js +498 -80
  12. package/dist/app.js.map +1 -1
  13. package/dist/attachment-storage.js +28 -12
  14. package/dist/attachment-storage.js.map +1 -1
  15. package/dist/backup-encryption.js +129 -0
  16. package/dist/backup-encryption.js.map +1 -0
  17. package/dist/character-extraction.js +133 -0
  18. package/dist/character-extraction.js.map +1 -0
  19. package/dist/cli-core.js +7 -6
  20. package/dist/cli-core.js.map +1 -1
  21. package/dist/collaboration-presence.js +200 -18
  22. package/dist/collaboration-presence.js.map +1 -1
  23. package/dist/database.js +335 -3
  24. package/dist/database.js.map +1 -1
  25. package/dist/docx-export.js +1 -1
  26. package/dist/docx-export.js.map +1 -1
  27. package/dist/domain.js +18 -0
  28. package/dist/domain.js.map +1 -1
  29. package/dist/epub-export.js +319 -0
  30. package/dist/epub-export.js.map +1 -0
  31. package/dist/hybrid-search.js +8 -0
  32. package/dist/hybrid-search.js.map +1 -1
  33. package/dist/image-metadata.js +17 -2
  34. package/dist/image-metadata.js.map +1 -1
  35. package/dist/presence-store.js +160 -0
  36. package/dist/presence-store.js.map +1 -0
  37. package/dist/public/ai-connectivity-test.d.ts +7 -0
  38. package/dist/public/ai-connectivity-test.js +82 -0
  39. package/dist/public/ai-context-meter.js +27 -0
  40. package/dist/public/ai-mentions.js +14 -0
  41. package/dist/public/ai-request-manager.js +99 -0
  42. package/dist/public/ai-stream-protocol.js +51 -0
  43. package/dist/public/app.js +3587 -559
  44. package/dist/public/background-task-center.js +1 -1
  45. package/dist/public/chapter-editor-virtualization.js +52 -0
  46. package/dist/public/chapter-version-diff.d.ts +20 -0
  47. package/dist/public/chapter-version-diff.js +116 -0
  48. package/dist/public/foreshadow-reminder.d.ts +32 -0
  49. package/dist/public/foreshadow-reminder.js +73 -0
  50. package/dist/public/global-replace-refresh.js +60 -0
  51. package/dist/public/index.html +161 -31
  52. package/dist/public/outline-board.d.ts +61 -0
  53. package/dist/public/outline-board.js +137 -0
  54. package/dist/public/page-route.d.ts +1 -0
  55. package/dist/public/page-route.js +8 -0
  56. package/dist/public/presence-client-id.d.ts +10 -0
  57. package/dist/public/presence-client-id.js +31 -0
  58. package/dist/public/reading-preview.d.ts +32 -0
  59. package/dist/public/reading-preview.js +136 -0
  60. package/dist/public/s3-backup-ui.d.ts +10 -0
  61. package/dist/public/s3-backup-ui.js +29 -0
  62. package/dist/public/setting-filters.d.ts +16 -0
  63. package/dist/public/setting-filters.js +19 -0
  64. package/dist/public/styles.css +522 -11
  65. package/dist/public/upload-progress.d.ts +2 -0
  66. package/dist/public/upload-progress.js +10 -0
  67. package/dist/s3-backup.js +197 -12
  68. package/dist/s3-backup.js.map +1 -1
  69. package/dist/security.js +86 -21
  70. package/dist/security.js.map +1 -1
  71. package/dist/server-runtime.js +38 -18
  72. package/dist/server-runtime.js.map +1 -1
  73. package/dist/store.js +899 -104
  74. package/dist/store.js.map +1 -1
  75. package/dist/upload-limits.js +35 -0
  76. package/dist/upload-limits.js.map +1 -0
  77. package/dist/user-auth.js +56 -10
  78. package/dist/user-auth.js.map +1 -1
  79. package/dist/utils.js +3 -0
  80. package/dist/utils.js.map +1 -1
  81. package/dist/version.js +1 -1
  82. package/package.json +4 -2
@@ -0,0 +1,82 @@
1
+ const objectLabels = Object.freeze({ provider: "供应商", model: "模型" });
2
+
3
+ function record(value) {
4
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
5
+ }
6
+
7
+ function objectLabel(objectType) {
8
+ return objectLabels[objectType] ?? "AI 配置";
9
+ }
10
+
11
+ function retryTimeText(retryAt) {
12
+ if (typeof retryAt !== "string" || !retryAt) return "";
13
+ const date = new Date(retryAt);
14
+ if (!Number.isFinite(date.getTime())) return "";
15
+ return `,可于 ${date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false })} 再次测试`;
16
+ }
17
+
18
+ function remainingSeconds(value, fallback) {
19
+ const seconds = Number(value);
20
+ return Number.isFinite(seconds) && seconds > 0 ? Math.ceil(seconds) : fallback;
21
+ }
22
+
23
+ export function connectivityTestResultToast(result, objectType) {
24
+ const source = record(result);
25
+ const cooldown = record(source.cooldown);
26
+ const label = objectLabel(objectType);
27
+ if (cooldown.reason === "configuration_changed") {
28
+ const outcome = source.ok === false && typeof source.error === "string"
29
+ ? `旧配置测试失败:${source.error};`
30
+ : "旧配置测试已完成;";
31
+ return {
32
+ message: `${label}${outcome}配置已在测试期间更新,旧结果不会锁定新配置,现在可以重新测试`,
33
+ type: "warning"
34
+ };
35
+ }
36
+ if (source.ok === true) {
37
+ const imageNotice = objectType === "model" && source.multimodalTested === true ? ",图片请求已验证" : "";
38
+ return {
39
+ message: `${label}连接测试成功${imageNotice};接下来 2 分钟内不能再次测试${retryTimeText(cooldown.retryAt)}`,
40
+ type: "info"
41
+ };
42
+ }
43
+ const failure = typeof source.error === "string" && source.error ? source.error : "未知错误";
44
+ return {
45
+ message: `${label}连接测试失败:${failure};10 秒后可以重试${retryTimeText(cooldown.retryAt)}`,
46
+ type: "error"
47
+ };
48
+ }
49
+
50
+ export function connectivityTestErrorToast(error, objectType) {
51
+ const source = record(error);
52
+ const details = record(source.details);
53
+ const label = objectLabel(objectType);
54
+ if (details.reason === "in_progress") {
55
+ const seconds = remainingSeconds(details.retryAfterSeconds, 1);
56
+ return {
57
+ message: `${label}已有连接测试正在进行,请等待约 ${seconds} 秒${retryTimeText(details.retryAt)}`,
58
+ type: "warning"
59
+ };
60
+ }
61
+ if (details.reason === "success_cooldown") {
62
+ const seconds = remainingSeconds(details.retryAfterSeconds, 120);
63
+ return {
64
+ message: `${label}仍在成功冷却中,剩余 ${seconds} 秒${retryTimeText(details.retryAt)}`,
65
+ type: "warning"
66
+ };
67
+ }
68
+ if (details.reason === "failure_cooldown") {
69
+ const seconds = remainingSeconds(details.retryAfterSeconds, 10);
70
+ return {
71
+ message: `${label}仍在失败冷却中,剩余 ${seconds} 秒${retryTimeText(details.retryAt)}`,
72
+ type: "warning"
73
+ };
74
+ }
75
+ const message = typeof source.message === "string" && source.message ? source.message : "请求失败";
76
+ return { message: `${label}连接测试失败:${message}`, type: "error" };
77
+ }
78
+
79
+ export function connectivityConfigurationSavedToast(objectType) {
80
+ const label = objectLabel(objectType);
81
+ return `${label}配置已保存,旧连接测试冷却已清除,现在可以重新测试`;
82
+ }
@@ -17,6 +17,33 @@ export function resolveAiContextUsage(previousUsage, nextUsage) {
17
17
  return nextUsage && typeof nextUsage === "object" ? nextUsage : previousUsage ?? null;
18
18
  }
19
19
 
20
+ const maximumAiContextUsageFields = [
21
+ "inputTokens",
22
+ "contextTokens",
23
+ "conversationTokens",
24
+ "usagePercent",
25
+ "conversationUsagePercent"
26
+ ];
27
+ const minimumAiContextUsageFields = ["remainingTokens"];
28
+
29
+ export function mergeAiContextUsage(previousUsage, nextUsage, allowShrink = false) {
30
+ if (!nextUsage || typeof nextUsage !== "object" || Array.isArray(nextUsage)) return previousUsage ?? null;
31
+ if (!previousUsage || typeof previousUsage !== "object" || Array.isArray(previousUsage) || allowShrink) return nextUsage;
32
+ const mergedUsage = { ...nextUsage };
33
+ for (const field of [...maximumAiContextUsageFields, ...minimumAiContextUsageFields]) {
34
+ const previousValue = Number(previousUsage[field]);
35
+ const nextValue = Number(nextUsage[field]);
36
+ if (Number.isFinite(previousValue) && Number.isFinite(nextValue)) {
37
+ mergedUsage[field] = minimumAiContextUsageFields.includes(field)
38
+ ? Math.min(previousValue, nextValue)
39
+ : Math.max(previousValue, nextValue);
40
+ }
41
+ else if (Number.isFinite(previousValue)) mergedUsage[field] = previousValue;
42
+ else if (Number.isFinite(nextValue)) mergedUsage[field] = nextValue;
43
+ }
44
+ return mergedUsage;
45
+ }
46
+
20
47
  export function formatAiContextUsagePercent(occupiedTokens, contextWindow) {
21
48
  const occupied = tokenCount(occupiedTokens);
22
49
  const window = tokenCount(contextWindow);
@@ -67,3 +67,17 @@ export function mergeAiReferenceScope(scope, references) {
67
67
  if (referenceScope.includeSettingInfo === true) merged.includeSettingInfo = true;
68
68
  return merged;
69
69
  }
70
+
71
+ export function userMessageMentionNames(mentionCharacterIds, characters) {
72
+ if (!Array.isArray(mentionCharacterIds) || !Array.isArray(characters)) return [];
73
+ const characterNames = new Map(characters.map((item) => [String(item?.id ?? ""), String(item?.name ?? "").trim()]));
74
+ const seen = new Set();
75
+ const names = [];
76
+ for (const characterId of mentionCharacterIds) {
77
+ if (typeof characterId !== "string" || seen.has(characterId)) continue;
78
+ seen.add(characterId);
79
+ const name = characterNames.get(characterId);
80
+ if (name) names.push(name);
81
+ }
82
+ return names;
83
+ }
@@ -0,0 +1,99 @@
1
+ function normalizedId(value) {
2
+ const normalized = String(value ?? "").trim();
3
+ return normalized || null;
4
+ }
5
+
6
+ export function createAiRequestAbortError(message = "AI 请求已取消") {
7
+ const error = new Error(message);
8
+ error.name = "AbortError";
9
+ error.code = "AI_REQUEST_CANCELLED";
10
+ return error;
11
+ }
12
+
13
+ export function isAiRequestCancellation(error, request = null) {
14
+ if (request?.signal?.aborted) return true;
15
+ return error?.name === "AbortError" || error?.code === "AI_REQUEST_CANCELLED";
16
+ }
17
+
18
+ export function aiRequestTargetsState(request, current) {
19
+ if (!request || normalizedId(current?.workId) !== request.workId) return false;
20
+ return request.conversationId === null
21
+ || normalizedId(current?.conversationId) === request.conversationId;
22
+ }
23
+
24
+ export function createAiRequestManager() {
25
+ let generation = 0;
26
+ let active = null;
27
+
28
+ const snapshot = (input, controller, requestGeneration) => Object.freeze({
29
+ generation: requestGeneration,
30
+ workId: normalizedId(input.workId),
31
+ conversationId: normalizedId(input.conversationId),
32
+ userMessageId: normalizedId(input.userMessageId),
33
+ signal: controller.signal
34
+ });
35
+
36
+ const isCurrent = (request) => Boolean(
37
+ request
38
+ && active
39
+ && !active.controller.signal.aborted
40
+ && active.snapshot.generation === request.generation
41
+ && active.controller.signal === request.signal
42
+ );
43
+
44
+ const cancel = (reason = "AI 请求已取消") => {
45
+ if (!active) return false;
46
+ const current = active;
47
+ active = null;
48
+ current.controller.abort(createAiRequestAbortError(reason));
49
+ return true;
50
+ };
51
+
52
+ const begin = (input) => {
53
+ cancel("新的 AI 请求已开始");
54
+ const controller = new AbortController();
55
+ generation += 1;
56
+ const request = snapshot(input, controller, generation);
57
+ if (!request.workId) throw new Error("AI 请求必须绑定作品");
58
+ active = { controller, snapshot: request };
59
+ return request;
60
+ };
61
+
62
+ const bind = (request, patch) => {
63
+ if (!isCurrent(request)) throw createAiRequestAbortError("AI 请求已失效");
64
+ const conversationId = Object.hasOwn(patch, "conversationId")
65
+ ? normalizedId(patch.conversationId)
66
+ : request.conversationId;
67
+ const userMessageId = Object.hasOwn(patch, "userMessageId")
68
+ ? normalizedId(patch.userMessageId)
69
+ : request.userMessageId;
70
+ if (request.conversationId && conversationId !== request.conversationId) {
71
+ throw new Error("AI 请求不能改绑到其他对话");
72
+ }
73
+ if (request.userMessageId && userMessageId !== request.userMessageId) {
74
+ throw new Error("AI 请求不能改绑到其他用户消息");
75
+ }
76
+ const next = snapshot({
77
+ workId: request.workId,
78
+ conversationId,
79
+ userMessageId
80
+ }, active.controller, request.generation);
81
+ active.snapshot = next;
82
+ return next;
83
+ };
84
+
85
+ const finish = (request) => {
86
+ if (!isCurrent(request)) return false;
87
+ active = null;
88
+ return true;
89
+ };
90
+
91
+ return {
92
+ begin,
93
+ bind,
94
+ cancel,
95
+ finish,
96
+ hasActive: () => active !== null,
97
+ isCurrent
98
+ };
99
+ }
@@ -0,0 +1,51 @@
1
+ function streamTransportError(code, message) {
2
+ const error = new Error(message);
3
+ error.code = code;
4
+ error.status = 502;
5
+ return error;
6
+ }
7
+
8
+ export function assertAiStreamCompleted(completed) {
9
+ if (completed) return;
10
+ throw streamTransportError(
11
+ "AI_STREAM_UPSTREAM_CLOSED",
12
+ "AI 流在收到完成事件前已关闭,已保留已生成内容"
13
+ );
14
+ }
15
+
16
+ export async function readAiEventStream(body, onEvent) {
17
+ const reader = body.getReader();
18
+ const decoder = new TextDecoder();
19
+ let buffer = "";
20
+ let completed = false;
21
+ const consume = async (eventText) => {
22
+ let eventName = "message";
23
+ const dataLines = [];
24
+ for (const line of eventText.split(/\r?\n/)) {
25
+ if (line.startsWith("event:")) eventName = line.slice(6).trim();
26
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
27
+ }
28
+ if (!dataLines.length) return;
29
+ const payload = JSON.parse(dataLines.join("\n"));
30
+ await onEvent(eventName, payload);
31
+ if (eventName === "complete") completed = true;
32
+ };
33
+ while (true) {
34
+ let chunk;
35
+ try {
36
+ chunk = await reader.read();
37
+ } catch {
38
+ throw streamTransportError(
39
+ "AI_STREAM_NETWORK_ERROR",
40
+ "AI 流连接中断,已保留已生成内容"
41
+ );
42
+ }
43
+ buffer += decoder.decode(chunk.value, { stream: !chunk.done });
44
+ const events = buffer.split(/\r?\n\r?\n/);
45
+ buffer = events.pop() ?? "";
46
+ for (const eventText of events) await consume(eventText);
47
+ if (chunk.done) break;
48
+ }
49
+ if (buffer.trim()) await consume(buffer);
50
+ return { completed };
51
+ }