@musnows/scriverse 0.2.0 → 0.3.1

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 (74) hide show
  1. package/README.en.md +13 -4
  2. package/README.md +13 -4
  3. package/dist/ai.js +3548 -0
  4. package/dist/ai.js.map +1 -0
  5. package/dist/app.js +1079 -0
  6. package/dist/app.js.map +1 -0
  7. package/dist/cli-core.js +147 -20
  8. package/dist/cli-core.js.map +1 -1
  9. package/dist/credential-vault.js +40 -0
  10. package/dist/credential-vault.js.map +1 -0
  11. package/dist/database.js +1220 -0
  12. package/dist/database.js.map +1 -0
  13. package/dist/docx-security.js +184 -0
  14. package/dist/docx-security.js.map +1 -0
  15. package/dist/domain.js +21 -0
  16. package/dist/domain.js.map +1 -0
  17. package/dist/errors.js +16 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/http-logging.js +76 -0
  20. package/dist/http-logging.js.map +1 -0
  21. package/dist/image-captcha.js +173 -0
  22. package/dist/image-captcha.js.map +1 -0
  23. package/dist/image-metadata.js +128 -0
  24. package/dist/image-metadata.js.map +1 -0
  25. package/dist/import-security.js +46 -0
  26. package/dist/import-security.js.map +1 -0
  27. package/dist/logger.js +110 -0
  28. package/dist/logger.js.map +1 -0
  29. package/dist/parser.js +223 -0
  30. package/dist/parser.js.map +1 -0
  31. package/dist/public/ai-context-meter.js +10 -0
  32. package/dist/public/ai-conversation.js +3 -0
  33. package/dist/public/ai-mentions.js +55 -0
  34. package/dist/public/ai-message-actions.js +27 -0
  35. package/dist/public/ai-message-meta.js +15 -0
  36. package/dist/public/ai-message-time.js +23 -0
  37. package/dist/public/ai-prompt-keyboard.js +3 -0
  38. package/dist/public/app.js +4435 -0
  39. package/dist/public/character-profile.d.ts +9 -0
  40. package/dist/public/character-profile.js +65 -0
  41. package/dist/public/character-version.d.ts +2 -0
  42. package/dist/public/character-version.js +31 -0
  43. package/dist/public/entity-version.js +34 -0
  44. package/dist/public/icon.svg +10 -0
  45. package/dist/public/index.html +547 -0
  46. package/dist/public/line-number-layout.js +15 -0
  47. package/dist/public/markdown.js +199 -0
  48. package/dist/public/model-config.d.ts +17 -0
  49. package/dist/public/model-config.js +57 -0
  50. package/dist/public/page-route.d.ts +11 -0
  51. package/dist/public/page-route.js +81 -0
  52. package/dist/public/relationship-graph.js +2017 -0
  53. package/dist/public/site.webmanifest +17 -0
  54. package/dist/public/styles.css +1441 -0
  55. package/dist/public/text-formatting.d.ts +2 -0
  56. package/dist/public/text-formatting.js +20 -0
  57. package/dist/public/theme-init.js +8 -0
  58. package/dist/public/theme.js +13 -0
  59. package/dist/public/whitespace-visualization.js +20 -0
  60. package/dist/request-context.js +16 -0
  61. package/dist/request-context.js.map +1 -0
  62. package/dist/security.js +247 -0
  63. package/dist/security.js.map +1 -0
  64. package/dist/server-runtime.js +90 -0
  65. package/dist/server-runtime.js.map +1 -0
  66. package/dist/server.js +26 -0
  67. package/dist/server.js.map +1 -0
  68. package/dist/store.js +2568 -0
  69. package/dist/store.js.map +1 -0
  70. package/dist/user-auth.js +515 -0
  71. package/dist/user-auth.js.map +1 -0
  72. package/dist/utils.js +67 -0
  73. package/dist/utils.js.map +1 -0
  74. package/package.json +4 -9
@@ -0,0 +1,128 @@
1
+ export class InvalidRasterImageError extends Error {
2
+ constructor(message = "图片文件无效") {
3
+ super(message);
4
+ this.name = "InvalidRasterImageError";
5
+ }
6
+ }
7
+ function assertDimensions(width, height) {
8
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
9
+ throw new InvalidRasterImageError("图片尺寸无效");
10
+ }
11
+ if (width > 4096 || height > 4096 || width * height > 16_777_216) {
12
+ throw new InvalidRasterImageError("图片尺寸不能超过 4096 × 4096 像素");
13
+ }
14
+ }
15
+ function readPng(bytes) {
16
+ const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
17
+ if (bytes.length < signature.length || !bytes.subarray(0, signature.length).equals(signature))
18
+ return null;
19
+ if (bytes.length < 33 || bytes.readUInt32BE(8) !== 13 || bytes.subarray(12, 16).toString("ascii") !== "IHDR") {
20
+ throw new InvalidRasterImageError("PNG 文件缺少有效的 IHDR 数据");
21
+ }
22
+ const width = bytes.readUInt32BE(16);
23
+ const height = bytes.readUInt32BE(20);
24
+ let offset = 8;
25
+ let hasEndChunk = false;
26
+ while (offset + 12 <= bytes.length) {
27
+ const chunkLength = bytes.readUInt32BE(offset);
28
+ const chunkEnd = offset + 12 + chunkLength;
29
+ if (chunkEnd > bytes.length)
30
+ throw new InvalidRasterImageError("PNG 文件结构不完整");
31
+ const chunkType = bytes.subarray(offset + 4, offset + 8).toString("ascii");
32
+ if (chunkType === "IEND") {
33
+ if (chunkLength !== 0)
34
+ throw new InvalidRasterImageError("PNG 文件的 IEND 数据无效");
35
+ hasEndChunk = true;
36
+ break;
37
+ }
38
+ offset = chunkEnd;
39
+ }
40
+ if (!hasEndChunk)
41
+ throw new InvalidRasterImageError("PNG 文件缺少结束标记");
42
+ assertDimensions(width, height);
43
+ return { mimeType: "image/png", width, height };
44
+ }
45
+ const jpegStartOfFrameMarkers = new Set([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf]);
46
+ function readJpeg(bytes) {
47
+ if (bytes.length < 3 || bytes[0] !== 0xff || bytes[1] !== 0xd8 || bytes[2] !== 0xff)
48
+ return null;
49
+ if (bytes.length < 4 || bytes[bytes.length - 2] !== 0xff || bytes[bytes.length - 1] !== 0xd9) {
50
+ throw new InvalidRasterImageError("JPEG 文件结构不完整");
51
+ }
52
+ let offset = 2;
53
+ while (offset + 1 < bytes.length) {
54
+ while (offset < bytes.length && bytes[offset] === 0xff)
55
+ offset += 1;
56
+ if (offset >= bytes.length)
57
+ break;
58
+ const marker = bytes[offset];
59
+ offset += 1;
60
+ if (marker === 0xd8 || marker === 0xd9 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7))
61
+ continue;
62
+ if (offset + 2 > bytes.length)
63
+ break;
64
+ const segmentLength = bytes.readUInt16BE(offset);
65
+ if (segmentLength < 2 || offset + segmentLength > bytes.length) {
66
+ throw new InvalidRasterImageError("JPEG 文件段长度无效");
67
+ }
68
+ if (jpegStartOfFrameMarkers.has(marker)) {
69
+ if (segmentLength < 7)
70
+ throw new InvalidRasterImageError("JPEG 尺寸数据无效");
71
+ const height = bytes.readUInt16BE(offset + 3);
72
+ const width = bytes.readUInt16BE(offset + 5);
73
+ assertDimensions(width, height);
74
+ return { mimeType: "image/jpeg", width, height };
75
+ }
76
+ if (marker === 0xda)
77
+ break;
78
+ offset += segmentLength;
79
+ }
80
+ throw new InvalidRasterImageError("JPEG 文件缺少尺寸数据");
81
+ }
82
+ function readUInt24LE(bytes, offset) {
83
+ return bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16);
84
+ }
85
+ function readWebp(bytes) {
86
+ if (bytes.length < 12 || bytes.subarray(0, 4).toString("ascii") !== "RIFF" || bytes.subarray(8, 12).toString("ascii") !== "WEBP")
87
+ return null;
88
+ const declaredLength = bytes.readUInt32LE(4) + 8;
89
+ if (declaredLength > bytes.length || declaredLength < 30)
90
+ throw new InvalidRasterImageError("WebP 文件结构不完整");
91
+ const chunkType = bytes.subarray(12, 16).toString("ascii");
92
+ const chunkLength = bytes.readUInt32LE(16);
93
+ if (20 + chunkLength > declaredLength)
94
+ throw new InvalidRasterImageError("WebP 图像块不完整");
95
+ let width;
96
+ let height;
97
+ if (chunkType === "VP8X") {
98
+ if (chunkLength < 10)
99
+ throw new InvalidRasterImageError("WebP 扩展头无效");
100
+ width = readUInt24LE(bytes, 24) + 1;
101
+ height = readUInt24LE(bytes, 27) + 1;
102
+ }
103
+ else if (chunkType === "VP8 ") {
104
+ if (chunkLength < 10 || bytes[23] !== 0x9d || bytes[24] !== 0x01 || bytes[25] !== 0x2a) {
105
+ throw new InvalidRasterImageError("WebP 有损图像头无效");
106
+ }
107
+ width = bytes.readUInt16LE(26) & 0x3fff;
108
+ height = bytes.readUInt16LE(28) & 0x3fff;
109
+ }
110
+ else if (chunkType === "VP8L") {
111
+ if (chunkLength < 5 || bytes[20] !== 0x2f)
112
+ throw new InvalidRasterImageError("WebP 无损图像头无效");
113
+ width = 1 + bytes[21] + ((bytes[22] & 0x3f) << 8);
114
+ height = 1 + (bytes[22] >> 6) + (bytes[23] << 2) + ((bytes[24] & 0x0f) << 10);
115
+ }
116
+ else {
117
+ throw new InvalidRasterImageError("WebP 文件缺少受支持的图像块");
118
+ }
119
+ assertDimensions(width, height);
120
+ return { mimeType: "image/webp", width, height };
121
+ }
122
+ export function readRasterImageMetadata(bytes) {
123
+ const metadata = readPng(bytes) ?? readJpeg(bytes) ?? readWebp(bytes);
124
+ if (!metadata)
125
+ throw new InvalidRasterImageError("仅支持 PNG、JPEG 或 WebP 图片");
126
+ return metadata;
127
+ }
128
+ //# sourceMappingURL=image-metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-metadata.js","sourceRoot":"","sources":["../src/image-metadata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { AppError } from "./errors.js";
2
+ const strictUtf8Decoder = new TextDecoder("utf-8", { fatal: true });
3
+ export function decodeUtf8ImportedText(value) {
4
+ try {
5
+ return strictUtf8Decoder.decode(value);
6
+ }
7
+ catch {
8
+ throw new AppError(415, "INVALID_TEXT_ENCODING", "TXT 文件必须使用 UTF-8 编码,请转换为 UTF-8 后重新上传");
9
+ }
10
+ }
11
+ function decodeEntitiesForInspection(value) {
12
+ const decodeCodePoint = (digits, radix) => {
13
+ const codePoint = Number.parseInt(digits, radix);
14
+ return Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : "";
15
+ };
16
+ return value
17
+ .replace(/&#x([0-9a-f]+);?/giu, (_match, digits) => decodeCodePoint(String(digits), 16))
18
+ .replace(/&#([0-9]+);?/gu, (_match, digits) => decodeCodePoint(String(digits), 10))
19
+ .replace(/&colon;/giu, ":")
20
+ .replace(/&(tab|newline);/giu, " ");
21
+ }
22
+ export function inspectImportedPlainText(value) {
23
+ const normalized = decodeEntitiesForInspection(String(value ?? "").normalize("NFKC").replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/gu, ""));
24
+ const compact = normalized.replace(/\s+/gu, "");
25
+ const threats = [];
26
+ if (/<\s*\/?\s*(script|iframe|object|embed|applet|meta|link|base|svg|math)\b/iu.test(normalized))
27
+ threats.push({ code: "ACTIVE_HTML_TAG" });
28
+ if (/<[^>]{0,4000}\bon[a-z][\w:-]*\s*=/iu.test(normalized))
29
+ threats.push({ code: "EVENT_HANDLER" });
30
+ if (/<[^>]{0,4000}(javascript|vbscript):/iu.test(compact))
31
+ threats.push({ code: "SCRIPT_URI" });
32
+ if (/<[^>]{0,4000}\bsrcdoc\s*=/iu.test(normalized) || /<[^>]{0,4000}data:text\/html/iu.test(compact))
33
+ threats.push({ code: "EMBEDDED_DOCUMENT" });
34
+ if (/<[^>]{0,4000}\bstyle\s*=[^>]{0,4000}(expression\s*\(|url\s*\(\s*(javascript|vbscript):)/iu.test(normalized))
35
+ threats.push({ code: "ACTIVE_CSS" });
36
+ return threats;
37
+ }
38
+ export function assertSafeImportedPlainText(value) {
39
+ const threats = inspectImportedPlainText(value);
40
+ if (threats.length) {
41
+ throw new AppError(422, "UNSAFE_IMPORT_CONTENT", "导入文件包含可能执行脚本的 HTML 内容,已拒绝导入", {
42
+ threats: threats.map((threat) => threat.code)
43
+ });
44
+ }
45
+ }
46
+ //# sourceMappingURL=import-security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-security.js","sourceRoot":"","sources":["../src/import-security.ts"],"names":[],"mappings":"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"}
package/dist/logger.js ADDED
@@ -0,0 +1,110 @@
1
+ import { createHash } from "node:crypto";
2
+ import { currentRequestContext } from "./request-context.js";
3
+ const levelPriority = {
4
+ debug: 10,
5
+ info: 20,
6
+ warn: 30,
7
+ error: 40,
8
+ silent: 100
9
+ };
10
+ const sensitiveField = /^(?:authorization|cookie|setcookie|password|passwordconfirmation|currentpassword|newpassword|secret|mastersecret|token|csrftoken|captchaanswer|apikey|credential|session|sessionid|username|displayname|email|account|userid|encryptedkey|keyiv|keytag)$/iu;
11
+ const sensitiveFieldPart = /(?:password|passwd|secret|authorization|cookie|csrf|captchaanswer|apikey|accesstoken|refreshtoken|sessiontoken|encryptedkey)/iu;
12
+ function scrubString(value) {
13
+ return value
14
+ .replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9+/=_\-.]+/giu, (match) => `${match.split(/\s/u)[0]} [REDACTED]`)
15
+ .replace(/\bscrv_[A-Za-z0-9_-]+\b/gu, "scrv_[REDACTED]")
16
+ .replace(/\bsk-[A-Za-z0-9_-]{8,}\b/gu, "sk-[REDACTED]")
17
+ .replace(/([?&](?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret|password)=)[^&\s]+/giu, "$1[REDACTED]")
18
+ .replace(/(https?:\/\/)[^\s/@:]+:[^\s/@]+@/giu, "$1[REDACTED]@")
19
+ .replace(/("(?:password|secret|token|api[_-]?key|authorization)"\s*:\s*")[^"]*(")/giu, "$1[REDACTED]$2");
20
+ }
21
+ function sanitizeValue(value, key = "", depth = 0, seen = new WeakSet()) {
22
+ const normalizedKey = key.replace(/[^a-z0-9]/giu, "");
23
+ if (sensitiveField.test(normalizedKey) || sensitiveFieldPart.test(normalizedKey))
24
+ return "[REDACTED]";
25
+ if (value === null || value === undefined || typeof value === "number" || typeof value === "boolean")
26
+ return value;
27
+ if (typeof value === "bigint")
28
+ return value.toString();
29
+ if (typeof value === "string")
30
+ return scrubString(value).slice(0, 4_000);
31
+ if (value instanceof Error)
32
+ return sanitizeError(value);
33
+ if (Buffer.isBuffer(value))
34
+ return { type: "Buffer", byteLength: value.byteLength };
35
+ if (depth >= 6)
36
+ return "[TRUNCATED]";
37
+ if (typeof value !== "object")
38
+ return String(value);
39
+ if (seen.has(value))
40
+ return "[CIRCULAR]";
41
+ seen.add(value);
42
+ if (Array.isArray(value))
43
+ return value.slice(0, 100).map((item) => sanitizeValue(item, "", depth + 1, seen));
44
+ return Object.fromEntries(Object.entries(value).slice(0, 100).map(([entryKey, entryValue]) => [entryKey, sanitizeValue(entryValue, entryKey, depth + 1, seen)]));
45
+ }
46
+ export function sanitizeLogFields(fields) {
47
+ return sanitizeValue(fields);
48
+ }
49
+ export function sanitizeError(error) {
50
+ if (!(error instanceof Error))
51
+ return { message: scrubString(String(error)).slice(0, 4_000) };
52
+ const candidate = error;
53
+ return {
54
+ name: error.name,
55
+ message: scrubString(error.message).slice(0, 4_000),
56
+ ...(candidate.code === undefined ? {} : { code: sanitizeValue(candidate.code, "code") }),
57
+ ...(candidate.status === undefined ? {} : { status: sanitizeValue(candidate.status, "status") }),
58
+ ...(error.stack ? { stack: scrubString(error.stack).slice(0, 12_000) } : {})
59
+ };
60
+ }
61
+ export function accountReference(userId) {
62
+ return `account:${createHash("sha256").update(userId).digest("hex").slice(0, 12)}`;
63
+ }
64
+ export function resolveLogLevel(environment = process.env) {
65
+ const configured = environment.LOG_LEVEL?.trim().toLocaleLowerCase();
66
+ if (configured === "debug" || configured === "info" || configured === "warn" || configured === "error" || configured === "silent")
67
+ return configured;
68
+ return environment.NODE_ENV === "test" ? "silent" : "info";
69
+ }
70
+ function defaultWrite(_level, record) {
71
+ // 服务和 CLI 共用日志器,统一写入 stderr,避免污染 CLI 的机器可读 stdout。
72
+ console.error(JSON.stringify(record));
73
+ }
74
+ export function createLogger(options = {}) {
75
+ const configuredLevel = options.level ?? resolveLogLevel();
76
+ const service = options.service ?? "scriverse";
77
+ const write = options.write ?? defaultWrite;
78
+ const now = options.now ?? (() => new Date());
79
+ const emit = (level, event, fields = {}) => {
80
+ if (levelPriority[level] < levelPriority[configuredLevel])
81
+ return;
82
+ const context = currentRequestContext();
83
+ const record = {
84
+ timestamp: now().toISOString(),
85
+ level,
86
+ service,
87
+ ...sanitizeLogFields(fields),
88
+ event,
89
+ ...(context?.requestId ? { requestId: context.requestId } : {}),
90
+ ...(context?.actor ? {
91
+ actorRef: accountReference(context.actor.userId),
92
+ ...(context.actor.authentication ? { authentication: context.actor.authentication } : {})
93
+ } : {})
94
+ };
95
+ try {
96
+ write(level, record);
97
+ }
98
+ catch {
99
+ // 日志输出失败不能中断业务请求。
100
+ }
101
+ };
102
+ return {
103
+ debug: (event, fields) => emit("debug", event, fields),
104
+ info: (event, fields) => emit("info", event, fields),
105
+ warn: (event, fields) => emit("warn", event, fields),
106
+ error: (event, fields) => emit("error", event, fields)
107
+ };
108
+ }
109
+ export const logger = createLogger();
110
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
package/dist/parser.js ADDED
@@ -0,0 +1,223 @@
1
+ import { countWords } from "./utils.js";
2
+ const volumePattern = /^\s*(?:第[零一二三四五六七八九十百千万两\d]+卷|卷[零一二三四五六七八九十百千万两\d]+)(?:\s+|[::]\s*|$)[^\n]*\s*$/u;
3
+ const chapterPattern = /^\s*(?:第[零一二三四五六七八九十百千万两\d]+章(?:[上中下])?|序章|楔子|终章|后记|作者的话)(?:\s+|[::]\s*|$)[^\n]*\s*$/u;
4
+ const specialVolumePattern = /^\s*(前传|附录)(?:\s+|[::]\s*|$)([^\n]*)\s*$/u;
5
+ function titleOf(line) {
6
+ return line.trim().replace(/\s+/gu, " ");
7
+ }
8
+ function kindOf(title) {
9
+ if (/^前传/u.test(title))
10
+ return "prequel";
11
+ if (/^番外/u.test(title))
12
+ return "extra";
13
+ if (/^附录/u.test(title))
14
+ return "appendix";
15
+ return "main";
16
+ }
17
+ function chapterTypeOf(title) {
18
+ return /^(?:后记|作者的话)/u.test(title) ? "作者的话" : "正文";
19
+ }
20
+ export function parseNovelText(raw) {
21
+ const text = raw.replace(/^\uFEFF/u, "").replace(/\r\n?/gu, "\n").trim();
22
+ const originalLines = text ? text.split("\n") : [];
23
+ const headingKey = (line) => {
24
+ const match = line.trim().match(/^(第[零一二三四五六七八九十百千万两\d]+[卷章]|卷[零一二三四五六七八九十百千万两\d]+|序章|楔子|终章|前传|番外|后记|附录)/u);
25
+ return match?.[1] ?? null;
26
+ };
27
+ const ignoredDirectoryLines = new Set();
28
+ const directoryIndexes = originalLines
29
+ .map((line, index) => /^\s*目\s*录\s*$/u.test(line) ? index : -1)
30
+ .filter((index) => index >= 0);
31
+ for (const directoryIndex of directoryIndexes) {
32
+ const pageHeadingKeys = new Set();
33
+ let pageEntryCount = 0;
34
+ let bodyBoundary = -1;
35
+ const scanLimit = Math.min(originalLines.length, directoryIndex + 5000);
36
+ for (let index = directoryIndex + 1; index < scanLimit; index += 1) {
37
+ const line = originalLines[index] ?? "";
38
+ const key = headingKey(line);
39
+ const hasPageNumber = /(?:\t\s*\d+| {2,}\d+)\s*$/u.test(line);
40
+ if (hasPageNumber) {
41
+ pageEntryCount += 1;
42
+ if (key)
43
+ pageHeadingKeys.add(key);
44
+ continue;
45
+ }
46
+ if (key && pageEntryCount >= 3 && pageHeadingKeys.size >= 2 && pageHeadingKeys.has(key)) {
47
+ bodyBoundary = index;
48
+ break;
49
+ }
50
+ }
51
+ if (bodyBoundary > directoryIndex) {
52
+ for (let index = directoryIndex; index < bodyBoundary; index += 1)
53
+ ignoredDirectoryLines.add(index);
54
+ continue;
55
+ }
56
+ let simpleEnd = directoryIndex + 1;
57
+ const simpleKeys = [];
58
+ let started = false;
59
+ while (simpleEnd < originalLines.length) {
60
+ const line = originalLines[simpleEnd] ?? "";
61
+ if (!line.trim()) {
62
+ if (started)
63
+ break;
64
+ simpleEnd += 1;
65
+ continue;
66
+ }
67
+ const key = headingKey(line);
68
+ if (!key)
69
+ break;
70
+ started = true;
71
+ simpleKeys.push(key);
72
+ simpleEnd += 1;
73
+ }
74
+ const laterKeys = new Set(originalLines.slice(simpleEnd).map(headingKey).filter((value) => Boolean(value)));
75
+ if (simpleKeys.length >= 2 && simpleKeys.some((key) => laterKeys.has(key))) {
76
+ for (let index = directoryIndex; index < simpleEnd; index += 1)
77
+ ignoredDirectoryLines.add(index);
78
+ }
79
+ }
80
+ const lines = originalLines.filter((_line, index) => !ignoredDirectoryLines.has(index));
81
+ // 卷标题附近必须出现目录或章节,并且同一卷号不能重复,避免把正文中的书名、篇名误当成分卷。
82
+ const volumeHeadingIndexes = new Set();
83
+ const seenVolumeKeys = new Set();
84
+ for (let index = 0; index < lines.length; index += 1) {
85
+ const line = lines[index] ?? "";
86
+ if (!volumePattern.test(line) && !specialVolumePattern.test(line))
87
+ continue;
88
+ const volumeKey = line.trim().match(/^(?:第([零一二三四五六七八九十百千万两\d]+)卷|卷([零一二三四五六七八九十百千万两\d]+))/u)?.slice(1).find(Boolean);
89
+ if (volumeKey && seenVolumeKeys.has(volumeKey))
90
+ continue;
91
+ let nextIndex = index + 1;
92
+ let meaningfulLineCount = 0;
93
+ while (nextIndex < lines.length && meaningfulLineCount < 8) {
94
+ const nextLine = lines[nextIndex] ?? "";
95
+ nextIndex += 1;
96
+ if (!nextLine.trim())
97
+ continue;
98
+ if (/^\s*目\s*录\s*$/u.test(nextLine) || chapterPattern.test(nextLine)) {
99
+ volumeHeadingIndexes.add(index);
100
+ if (volumeKey)
101
+ seenVolumeKeys.add(volumeKey);
102
+ break;
103
+ }
104
+ meaningfulLineCount += 1;
105
+ }
106
+ }
107
+ const hasExplicitVolume = volumeHeadingIndexes.size > 0;
108
+ const warnings = [];
109
+ const volumes = [];
110
+ let currentVolume;
111
+ let currentChapter;
112
+ let preamble = [];
113
+ const ensureVolume = () => {
114
+ if (!currentVolume) {
115
+ currentVolume = {
116
+ title: hasExplicitVolume ? "未归属内容" : "正文",
117
+ kind: "main",
118
+ source: "default",
119
+ order: volumes.length,
120
+ chapters: []
121
+ };
122
+ volumes.push(currentVolume);
123
+ }
124
+ return currentVolume;
125
+ };
126
+ const flushChapter = () => {
127
+ if (!currentChapter)
128
+ return;
129
+ currentChapter.content = currentChapter.content.trim();
130
+ ensureVolume().chapters.push(currentChapter);
131
+ currentChapter = undefined;
132
+ };
133
+ const flushPreamble = () => {
134
+ const content = preamble.join("\n").trim();
135
+ preamble = [];
136
+ if (!content)
137
+ return;
138
+ const volume = ensureVolume();
139
+ volume.chapters.push({
140
+ title: volume.chapters.length === 0 ? "卷首" : "未命名章节",
141
+ content,
142
+ order: volume.chapters.length,
143
+ chapterType: "正文"
144
+ });
145
+ };
146
+ for (const [lineIndex, line] of lines.entries()) {
147
+ if (volumeHeadingIndexes.has(lineIndex)) {
148
+ flushChapter();
149
+ flushPreamble();
150
+ const title = titleOf(line);
151
+ currentVolume = {
152
+ title,
153
+ kind: kindOf(title),
154
+ source: "explicit",
155
+ order: volumes.length,
156
+ chapters: []
157
+ };
158
+ volumes.push(currentVolume);
159
+ continue;
160
+ }
161
+ if (chapterPattern.test(line)) {
162
+ flushChapter();
163
+ flushPreamble();
164
+ const volume = ensureVolume();
165
+ currentChapter = {
166
+ title: titleOf(line),
167
+ content: "",
168
+ order: volume.chapters.length,
169
+ chapterType: chapterTypeOf(titleOf(line))
170
+ };
171
+ continue;
172
+ }
173
+ if (currentChapter) {
174
+ currentChapter.content += `${currentChapter.content ? "\n" : ""}${line}`;
175
+ }
176
+ else {
177
+ preamble.push(line);
178
+ }
179
+ }
180
+ flushChapter();
181
+ flushPreamble();
182
+ if (volumes.length === 0) {
183
+ volumes.push({
184
+ title: "正文",
185
+ kind: "main",
186
+ source: "default",
187
+ order: 0,
188
+ chapters: []
189
+ });
190
+ }
191
+ for (const volume of volumes) {
192
+ volume.chapters.forEach((chapter, index) => {
193
+ chapter.order = index;
194
+ });
195
+ }
196
+ if (!hasExplicitVolume)
197
+ warnings.push("未检测到明确卷标题,内容保留在默认卷中");
198
+ if (ignoredDirectoryLines.size > 0)
199
+ warnings.push(`已忽略目录中的 ${ignoredDirectoryLines.size} 行导航内容`);
200
+ if (volumes.some((volume) => volume.title === "未归属内容"))
201
+ warnings.push("卷标题前存在正文内容,已标记为未归属内容");
202
+ if (volumes.some((volume) => volume.chapters.length === 0))
203
+ warnings.push("检测到空卷");
204
+ return {
205
+ volumes,
206
+ warnings,
207
+ wordCount: countWords(text),
208
+ paragraphCount: originalLines.filter((line) => line.trim()).length
209
+ };
210
+ }
211
+ export function applyImportFileHints(parsed, fileName) {
212
+ if (!/前传/u.test(fileName) || !parsed.volumes.some((volume) => volume.source === "explicit"))
213
+ return parsed;
214
+ const leadingDefaultVolume = parsed.volumes.find((volume) => volume.source === "default");
215
+ if (!leadingDefaultVolume)
216
+ return parsed;
217
+ leadingDefaultVolume.title = "前传";
218
+ leadingDefaultVolume.kind = "prequel";
219
+ parsed.warnings = parsed.warnings.filter((warning) => warning !== "卷标题前存在正文内容,已标记为未归属内容");
220
+ parsed.warnings.push("根据文件名将首个未分卷内容识别为前传");
221
+ return parsed;
222
+ }
223
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export function formatAiContextUsageTooltip(usage) {
2
+ if (!usage) return "选择可用模型后显示当前上下文用量";
3
+ const inputTokens = Math.max(0, Math.round(Number(usage.inputTokens) || 0)).toLocaleString("zh-CN");
4
+ const contextWindow = Math.max(0, Math.round(Number(usage.contextWindow) || 0)).toLocaleString("zh-CN");
5
+ const contextTokens = Math.max(0, Math.round(Number(usage.contextTokens) || 0)).toLocaleString("zh-CN");
6
+ const conversationTokens = Math.max(0, Math.round(Number(usage.conversationTokens) || 0)).toLocaleString("zh-CN");
7
+ const conversationBudget = Math.max(0, Math.round(Number(usage.conversationBudgetTokens) || 0)).toLocaleString("zh-CN");
8
+ const outputReserve = Math.max(0, Math.round(Number(usage.outputReserveTokens) || 0)).toLocaleString("zh-CN");
9
+ return `总输入 ${inputTokens} / ${contextWindow} tok · 作品上下文 ${contextTokens} tok · 对话历史 ${conversationTokens} / ${conversationBudget} tok · 输出预留 ${outputReserve} tok`;
10
+ }
@@ -0,0 +1,3 @@
1
+ export function shouldShowAiQuickActions(hasSentPrompt) {
2
+ return !Boolean(hasSentPrompt);
3
+ }
@@ -0,0 +1,55 @@
1
+ export function findAiMention(value, cursor = value.length) {
2
+ const safeCursor = Math.max(0, Math.min(Number(cursor) || 0, value.length));
3
+ const match = value.slice(0, safeCursor).match(/@([^@\s]*)$/u);
4
+ if (!match) return null;
5
+ return {
6
+ start: safeCursor - match[0].length,
7
+ end: safeCursor,
8
+ query: match[1]
9
+ };
10
+ }
11
+
12
+ export function listAiMentionOptions(characters, settings, chapters, query = "", limit = 12) {
13
+ const keyword = String(query).trim().toLocaleLowerCase("zh-CN");
14
+ const groups = [
15
+ characters.map((item) => ({ kind: "character", kindLabel: "角色", id: String(item.id), name: String(item.name) })),
16
+ settings.map((item) => ({ kind: "setting", kindLabel: "设定", id: String(item.id), name: String(item.title) })),
17
+ chapters.map((item) => ({
18
+ kind: "chapter",
19
+ kindLabel: "章节",
20
+ id: String(item.id),
21
+ name: `${String(item.volumeTitle)} / ${String(item.title)}`
22
+ }))
23
+ ];
24
+ const maximum = Math.max(1, Number(limit) || 12);
25
+ if (keyword) {
26
+ return groups.flat()
27
+ .filter((item) => item.name.toLocaleLowerCase("zh-CN").includes(keyword))
28
+ .slice(0, maximum);
29
+ }
30
+ const options = [];
31
+ for (let index = 0; options.length < maximum && groups.some((group) => group[index]); index += 1) {
32
+ for (const group of groups) {
33
+ if (group[index]) options.push(group[index]);
34
+ if (options.length === maximum) break;
35
+ }
36
+ }
37
+ return options;
38
+ }
39
+
40
+ export function applyAiMention(value, mention, name) {
41
+ const token = `@${String(name).trim()}`;
42
+ const text = `${value.slice(0, mention.start)}${token} ${value.slice(mention.end)}`;
43
+ return { text, token, cursor: mention.start + token.length + 1 };
44
+ }
45
+
46
+ export function buildAiReferenceScope(references) {
47
+ const chapterIds = [...new Set(references.filter((item) => item.kind === "chapter").map((item) => item.id))];
48
+ const characterIds = [...new Set(references.filter((item) => item.kind === "character").map((item) => item.id))];
49
+ const settingIds = [...new Set(references.filter((item) => item.kind === "setting").map((item) => item.id))];
50
+ return {
51
+ ...(chapterIds.length ? { chapterIds } : {}),
52
+ ...(characterIds.length ? { characterIds } : {}),
53
+ ...(settingIds.length ? { settingIds } : {})
54
+ };
55
+ }