@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.
- package/README.en.md +3 -0
- package/README.md +8 -0
- package/dist/ai-connectivity-test.js +109 -0
- package/dist/ai-connectivity-test.js.map +1 -0
- package/dist/ai-conversation-export.js +70 -0
- package/dist/ai-conversation-export.js.map +1 -0
- package/dist/ai-stream-timeout.js +18 -0
- package/dist/ai-stream-timeout.js.map +1 -0
- package/dist/ai.js +926 -354
- package/dist/ai.js.map +1 -1
- package/dist/app.js +498 -80
- package/dist/app.js.map +1 -1
- package/dist/attachment-storage.js +28 -12
- package/dist/attachment-storage.js.map +1 -1
- package/dist/backup-encryption.js +129 -0
- package/dist/backup-encryption.js.map +1 -0
- package/dist/character-extraction.js +133 -0
- package/dist/character-extraction.js.map +1 -0
- package/dist/cli-core.js +7 -6
- package/dist/cli-core.js.map +1 -1
- package/dist/collaboration-presence.js +200 -18
- package/dist/collaboration-presence.js.map +1 -1
- package/dist/database.js +335 -3
- package/dist/database.js.map +1 -1
- package/dist/docx-export.js +1 -1
- package/dist/docx-export.js.map +1 -1
- package/dist/domain.js +18 -0
- package/dist/domain.js.map +1 -1
- package/dist/epub-export.js +319 -0
- package/dist/epub-export.js.map +1 -0
- package/dist/hybrid-search.js +8 -0
- package/dist/hybrid-search.js.map +1 -1
- package/dist/image-metadata.js +17 -2
- package/dist/image-metadata.js.map +1 -1
- package/dist/presence-store.js +160 -0
- package/dist/presence-store.js.map +1 -0
- package/dist/public/ai-connectivity-test.d.ts +7 -0
- package/dist/public/ai-connectivity-test.js +82 -0
- package/dist/public/ai-context-meter.js +27 -0
- package/dist/public/ai-mentions.js +14 -0
- package/dist/public/ai-request-manager.js +99 -0
- package/dist/public/ai-stream-protocol.js +51 -0
- package/dist/public/app.js +3587 -559
- package/dist/public/background-task-center.js +1 -1
- package/dist/public/chapter-editor-virtualization.js +52 -0
- package/dist/public/chapter-version-diff.d.ts +20 -0
- package/dist/public/chapter-version-diff.js +116 -0
- package/dist/public/foreshadow-reminder.d.ts +32 -0
- package/dist/public/foreshadow-reminder.js +73 -0
- package/dist/public/global-replace-refresh.js +60 -0
- package/dist/public/index.html +161 -31
- package/dist/public/outline-board.d.ts +61 -0
- package/dist/public/outline-board.js +137 -0
- package/dist/public/page-route.d.ts +1 -0
- package/dist/public/page-route.js +8 -0
- package/dist/public/presence-client-id.d.ts +10 -0
- package/dist/public/presence-client-id.js +31 -0
- package/dist/public/reading-preview.d.ts +32 -0
- package/dist/public/reading-preview.js +136 -0
- package/dist/public/s3-backup-ui.d.ts +10 -0
- package/dist/public/s3-backup-ui.js +29 -0
- package/dist/public/setting-filters.d.ts +16 -0
- package/dist/public/setting-filters.js +19 -0
- package/dist/public/styles.css +522 -11
- package/dist/public/upload-progress.d.ts +2 -0
- package/dist/public/upload-progress.js +10 -0
- package/dist/s3-backup.js +197 -12
- package/dist/s3-backup.js.map +1 -1
- package/dist/security.js +86 -21
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +38 -18
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +899 -104
- package/dist/store.js.map +1 -1
- package/dist/upload-limits.js +35 -0
- package/dist/upload-limits.js.map +1 -0
- package/dist/user-auth.js +56 -10
- package/dist/user-auth.js.map +1 -1
- package/dist/utils.js +3 -0
- package/dist/utils.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -2
|
@@ -4,6 +4,7 @@ import { copyFile, mkdir, open, readFile, rm, stat } from "node:fs/promises";
|
|
|
4
4
|
import { dirname, join, resolve, sep } from "node:path";
|
|
5
5
|
import sharp from "sharp";
|
|
6
6
|
import { AppError } from "./errors.js";
|
|
7
|
+
import { DEFAULT_ATTACHMENT_IMAGE_MAX_BYTES, formatUploadLimit } from "./upload-limits.js";
|
|
7
8
|
const maximumPixels = 25_000_000;
|
|
8
9
|
const maximumAnimationFrames = 100;
|
|
9
10
|
const maximumAnimationPixels = 50_000_000;
|
|
@@ -41,11 +42,27 @@ async function sha256File(path) {
|
|
|
41
42
|
}
|
|
42
43
|
return hash.digest("hex");
|
|
43
44
|
}
|
|
45
|
+
async function hasGifSignature(path) {
|
|
46
|
+
const handle = await open(path, "r");
|
|
47
|
+
const header = Buffer.alloc(6);
|
|
48
|
+
try {
|
|
49
|
+
const { bytesRead } = await handle.read(header, 0, header.length, 0);
|
|
50
|
+
if (bytesRead !== header.length)
|
|
51
|
+
return false;
|
|
52
|
+
const signature = header.toString("ascii");
|
|
53
|
+
return signature === "GIF87a" || signature === "GIF89a";
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
await handle.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
44
59
|
export class AttachmentStorage {
|
|
45
60
|
rootDirectory;
|
|
61
|
+
maximumUploadBytes;
|
|
46
62
|
temporaryDirectory;
|
|
47
|
-
constructor(rootDirectory) {
|
|
63
|
+
constructor(rootDirectory, maximumUploadBytes = DEFAULT_ATTACHMENT_IMAGE_MAX_BYTES) {
|
|
48
64
|
this.rootDirectory = rootDirectory;
|
|
65
|
+
this.maximumUploadBytes = maximumUploadBytes;
|
|
49
66
|
this.temporaryDirectory = join(rootDirectory, ".tmp");
|
|
50
67
|
}
|
|
51
68
|
async prepare() {
|
|
@@ -82,10 +99,14 @@ export class AttachmentStorage {
|
|
|
82
99
|
async ingest(sourcePath) {
|
|
83
100
|
await this.prepare();
|
|
84
101
|
const originalStats = await stat(sourcePath);
|
|
102
|
+
if (originalStats.size > this.maximumUploadBytes) {
|
|
103
|
+
throw new AppError(413, "ATTACHMENT_TOO_LARGE", `图片附件不能超过 ${formatUploadLimit(this.maximumUploadBytes)}`);
|
|
104
|
+
}
|
|
85
105
|
const originalSha256 = await sha256File(sourcePath);
|
|
106
|
+
const isGif = await hasGifSignature(sourcePath);
|
|
86
107
|
let metadata;
|
|
87
108
|
try {
|
|
88
|
-
metadata = await sharp(sourcePath, { animated: true, limitInputPixels: maximumPixels, sequentialRead: true }).metadata();
|
|
109
|
+
metadata = await sharp(sourcePath, { animated: true, limitInputPixels: isGif ? false : maximumPixels, sequentialRead: true }).metadata();
|
|
89
110
|
}
|
|
90
111
|
catch {
|
|
91
112
|
throw new AppError(415, "INVALID_ATTACHMENT_IMAGE", "附件不是有效的图片文件");
|
|
@@ -99,19 +120,19 @@ export class AttachmentStorage {
|
|
|
99
120
|
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(pageHeight) || pageHeight <= 0) {
|
|
100
121
|
throw new AppError(415, "INVALID_ATTACHMENT_IMAGE", "无法读取附件图片尺寸");
|
|
101
122
|
}
|
|
102
|
-
if (width * pageHeight > maximumPixels)
|
|
123
|
+
if (!isGif && width * pageHeight > maximumPixels)
|
|
103
124
|
throw new AppError(413, "ATTACHMENT_IMAGE_TOO_LARGE", "附件图片像素尺寸过大");
|
|
104
|
-
if (!Number.isInteger(pageCount) || pageCount > maximumAnimationFrames) {
|
|
125
|
+
if (!isGif && (!Number.isInteger(pageCount) || pageCount > maximumAnimationFrames)) {
|
|
105
126
|
throw new AppError(413, "ATTACHMENT_ANIMATION_TOO_LARGE", "附件动画帧数过多");
|
|
106
127
|
}
|
|
107
|
-
if (width * pageHeight * pageCount > maximumAnimationPixels) {
|
|
128
|
+
if (!isGif && width * pageHeight * pageCount > maximumAnimationPixels) {
|
|
108
129
|
throw new AppError(413, "ATTACHMENT_ANIMATION_TOO_LARGE", "附件动画总像素量过大");
|
|
109
130
|
}
|
|
110
131
|
const originalMimeType = mimeType(format);
|
|
111
132
|
const candidatePath = join(this.temporaryDirectory, `${originalSha256}-${Date.now()}.webp`);
|
|
112
133
|
let selectedPath = sourcePath;
|
|
113
134
|
let storedMimeType = originalMimeType;
|
|
114
|
-
if (format !== "webp") {
|
|
135
|
+
if (format !== "webp" && !isGif) {
|
|
115
136
|
try {
|
|
116
137
|
await sharp(sourcePath, { animated: true, limitInputPixels: maximumPixels, sequentialRead: true })
|
|
117
138
|
.webp({ lossless: true, effort: 6 })
|
|
@@ -126,13 +147,8 @@ export class AttachmentStorage {
|
|
|
126
147
|
storedMimeType = "image/webp";
|
|
127
148
|
}
|
|
128
149
|
}
|
|
129
|
-
catch
|
|
150
|
+
catch {
|
|
130
151
|
await rm(candidatePath, { force: true });
|
|
131
|
-
if (format === "gif") {
|
|
132
|
-
if (error instanceof AppError)
|
|
133
|
-
throw error;
|
|
134
|
-
throw new AppError(422, "ATTACHMENT_ANIMATION_CONVERSION_FAILED", "GIF 附件无法安全转换为动画 WebP");
|
|
135
|
-
}
|
|
136
152
|
}
|
|
137
153
|
}
|
|
138
154
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-storage.js","sourceRoot":"","sources":["../src/attachment-storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,IAAI,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,KAAwB,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"attachment-storage.js","sourceRoot":"","sources":["../src/attachment-storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
2
|
+
import { AppError } from "./errors.js";
|
|
3
|
+
export const BACKUP_ENCRYPTION_MAGIC = "SCRIVERSE-ENC1";
|
|
4
|
+
const magic = Buffer.from(BACKUP_ENCRYPTION_MAGIC, "ascii");
|
|
5
|
+
const envelopeVersion = 1;
|
|
6
|
+
const aes256GcmAlgorithm = 1;
|
|
7
|
+
const dekLength = 32;
|
|
8
|
+
const ivLength = 12;
|
|
9
|
+
const authTagLength = 16;
|
|
10
|
+
const headerLength = magic.byteLength + 16;
|
|
11
|
+
function parseKek(value) {
|
|
12
|
+
if (!/^[A-Za-z0-9_-]{43}$/u.test(value)) {
|
|
13
|
+
throw new AppError(400, "BACKUP_KEK_INVALID", "S3 备份加密密钥格式无效");
|
|
14
|
+
}
|
|
15
|
+
const key = Buffer.from(value, "base64url");
|
|
16
|
+
if (key.byteLength !== 32 || key.toString("base64url") !== value) {
|
|
17
|
+
throw new AppError(400, "BACKUP_KEK_INVALID", "S3 备份加密密钥格式无效");
|
|
18
|
+
}
|
|
19
|
+
return key;
|
|
20
|
+
}
|
|
21
|
+
function envelopeHeader(ciphertextLength) {
|
|
22
|
+
const header = Buffer.alloc(headerLength);
|
|
23
|
+
magic.copy(header, 0);
|
|
24
|
+
let offset = magic.byteLength;
|
|
25
|
+
header.writeUInt8(envelopeVersion, offset++);
|
|
26
|
+
header.writeUInt8(aes256GcmAlgorithm, offset++);
|
|
27
|
+
header.writeUInt8(ivLength, offset++);
|
|
28
|
+
header.writeUInt8(authTagLength, offset++);
|
|
29
|
+
header.writeUInt8(ivLength, offset++);
|
|
30
|
+
header.writeUInt8(authTagLength, offset++);
|
|
31
|
+
header.writeUInt16BE(dekLength, offset);
|
|
32
|
+
offset += 2;
|
|
33
|
+
header.writeBigUInt64BE(BigInt(ciphertextLength), offset);
|
|
34
|
+
return header;
|
|
35
|
+
}
|
|
36
|
+
export function generateKek() {
|
|
37
|
+
return randomBytes(32).toString("base64url");
|
|
38
|
+
}
|
|
39
|
+
export function isEncryptedEnvelope(value) {
|
|
40
|
+
return value.byteLength >= magic.byteLength && value.subarray(0, magic.byteLength).equals(magic);
|
|
41
|
+
}
|
|
42
|
+
export function encryptObject(plaintext, kek) {
|
|
43
|
+
const key = parseKek(kek);
|
|
44
|
+
const dek = randomBytes(dekLength);
|
|
45
|
+
const wrappingIv = randomBytes(ivLength);
|
|
46
|
+
const payloadIv = randomBytes(ivLength);
|
|
47
|
+
const header = envelopeHeader(plaintext.byteLength);
|
|
48
|
+
const keyCipher = createCipheriv("aes-256-gcm", key, wrappingIv);
|
|
49
|
+
keyCipher.setAAD(header);
|
|
50
|
+
const wrappedDek = Buffer.concat([keyCipher.update(dek), keyCipher.final()]);
|
|
51
|
+
const keyTag = keyCipher.getAuthTag();
|
|
52
|
+
const payloadCipher = createCipheriv("aes-256-gcm", dek, payloadIv);
|
|
53
|
+
payloadCipher.setAAD(header);
|
|
54
|
+
const ciphertext = Buffer.concat([payloadCipher.update(plaintext), payloadCipher.final()]);
|
|
55
|
+
const payloadTag = payloadCipher.getAuthTag();
|
|
56
|
+
return Buffer.concat([
|
|
57
|
+
header,
|
|
58
|
+
wrappingIv,
|
|
59
|
+
keyTag,
|
|
60
|
+
payloadIv,
|
|
61
|
+
payloadTag,
|
|
62
|
+
wrappedDek,
|
|
63
|
+
ciphertext
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
export function decryptObject(envelope, kek) {
|
|
67
|
+
if (envelope.byteLength < headerLength || !isEncryptedEnvelope(envelope)) {
|
|
68
|
+
throw new AppError(400, "BACKUP_ENVELOPE_INVALID", "备份对象不是 Scriverse 加密信封");
|
|
69
|
+
}
|
|
70
|
+
const header = envelope.subarray(0, headerLength);
|
|
71
|
+
let offset = magic.byteLength;
|
|
72
|
+
const version = header.readUInt8(offset++);
|
|
73
|
+
const algorithm = header.readUInt8(offset++);
|
|
74
|
+
const wrappingIvLength = header.readUInt8(offset++);
|
|
75
|
+
const wrappingTagLength = header.readUInt8(offset++);
|
|
76
|
+
const payloadIvLength = header.readUInt8(offset++);
|
|
77
|
+
const payloadTagLength = header.readUInt8(offset++);
|
|
78
|
+
const wrappedDekLength = header.readUInt16BE(offset);
|
|
79
|
+
offset += 2;
|
|
80
|
+
const ciphertextLengthBigInt = header.readBigUInt64BE(offset);
|
|
81
|
+
if (ciphertextLengthBigInt > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
82
|
+
throw new AppError(400, "BACKUP_ENVELOPE_INVALID", "备份加密信封长度无效");
|
|
83
|
+
}
|
|
84
|
+
const ciphertextLength = Number(ciphertextLengthBigInt);
|
|
85
|
+
if (version !== envelopeVersion
|
|
86
|
+
|| algorithm !== aes256GcmAlgorithm
|
|
87
|
+
|| wrappingIvLength !== ivLength
|
|
88
|
+
|| wrappingTagLength !== authTagLength
|
|
89
|
+
|| payloadIvLength !== ivLength
|
|
90
|
+
|| payloadTagLength !== authTagLength
|
|
91
|
+
|| wrappedDekLength !== dekLength) {
|
|
92
|
+
throw new AppError(400, "BACKUP_ENVELOPE_UNSUPPORTED", "备份加密信封版本或算法不受支持");
|
|
93
|
+
}
|
|
94
|
+
const expectedLength = headerLength
|
|
95
|
+
+ wrappingIvLength
|
|
96
|
+
+ wrappingTagLength
|
|
97
|
+
+ payloadIvLength
|
|
98
|
+
+ payloadTagLength
|
|
99
|
+
+ wrappedDekLength
|
|
100
|
+
+ ciphertextLength;
|
|
101
|
+
if (expectedLength !== envelope.byteLength) {
|
|
102
|
+
throw new AppError(400, "BACKUP_ENVELOPE_INVALID", "备份加密信封长度无效");
|
|
103
|
+
}
|
|
104
|
+
let bodyOffset = headerLength;
|
|
105
|
+
const wrappingIv = envelope.subarray(bodyOffset, bodyOffset += wrappingIvLength);
|
|
106
|
+
const wrappingTag = envelope.subarray(bodyOffset, bodyOffset += wrappingTagLength);
|
|
107
|
+
const payloadIv = envelope.subarray(bodyOffset, bodyOffset += payloadIvLength);
|
|
108
|
+
const payloadTag = envelope.subarray(bodyOffset, bodyOffset += payloadTagLength);
|
|
109
|
+
const wrappedDek = envelope.subarray(bodyOffset, bodyOffset += wrappedDekLength);
|
|
110
|
+
const ciphertext = envelope.subarray(bodyOffset);
|
|
111
|
+
try {
|
|
112
|
+
const keyDecipher = createDecipheriv("aes-256-gcm", parseKek(kek), wrappingIv);
|
|
113
|
+
keyDecipher.setAAD(header);
|
|
114
|
+
keyDecipher.setAuthTag(wrappingTag);
|
|
115
|
+
const dek = Buffer.concat([keyDecipher.update(wrappedDek), keyDecipher.final()]);
|
|
116
|
+
if (dek.byteLength !== dekLength)
|
|
117
|
+
throw new Error("Invalid DEK length");
|
|
118
|
+
const payloadDecipher = createDecipheriv("aes-256-gcm", dek, payloadIv);
|
|
119
|
+
payloadDecipher.setAAD(header);
|
|
120
|
+
payloadDecipher.setAuthTag(payloadTag);
|
|
121
|
+
return Buffer.concat([payloadDecipher.update(ciphertext), payloadDecipher.final()]);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof AppError)
|
|
125
|
+
throw error;
|
|
126
|
+
throw new AppError(400, "BACKUP_DECRYPTION_FAILED", "备份对象解密失败,密钥错误或对象已损坏");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=backup-encryption.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backup-encryption.js","sourceRoot":"","sources":["../src/backup-encryption.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AppError } from "./errors.js";
|
|
3
|
+
import { normalizeCharacterName } from "./store.js";
|
|
4
|
+
export const CHARACTER_EXTRACTION_MAX_CANDIDATES = 500;
|
|
5
|
+
export const CHARACTER_EXTRACTION_MAX_NAME_LENGTH = 200;
|
|
6
|
+
export const CHARACTER_EXTRACTION_MAX_ALIASES = 100;
|
|
7
|
+
export const CHARACTER_EXTRACTION_MAX_IDENTITY_LENGTH = 20_000;
|
|
8
|
+
export const CHARACTER_EXTRACTION_MAX_SPECIES_LENGTH = 200;
|
|
9
|
+
function recordValue(value) {
|
|
10
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
11
|
+
? value
|
|
12
|
+
: null;
|
|
13
|
+
}
|
|
14
|
+
export function normalizeCharacterExtractionText(value) {
|
|
15
|
+
return typeof value === "string" ? value.normalize("NFKC").trim().replace(/\s+/gu, " ") : "";
|
|
16
|
+
}
|
|
17
|
+
export function normalizeCharacterExtractionAliases(name, value) {
|
|
18
|
+
if (!Array.isArray(value))
|
|
19
|
+
return [];
|
|
20
|
+
const primary = normalizeCharacterName(name);
|
|
21
|
+
const aliases = new Map();
|
|
22
|
+
for (const item of value.slice(0, CHARACTER_EXTRACTION_MAX_ALIASES)) {
|
|
23
|
+
const alias = normalizeCharacterExtractionText(item);
|
|
24
|
+
if (!alias || alias.length > CHARACTER_EXTRACTION_MAX_NAME_LENGTH)
|
|
25
|
+
continue;
|
|
26
|
+
const normalized = normalizeCharacterName(alias);
|
|
27
|
+
if (!normalized || normalized === primary || aliases.has(normalized))
|
|
28
|
+
continue;
|
|
29
|
+
aliases.set(normalized, alias);
|
|
30
|
+
}
|
|
31
|
+
return [...aliases.values()];
|
|
32
|
+
}
|
|
33
|
+
export function normalizeCharacterExtractionCandidate(value, index) {
|
|
34
|
+
const candidate = recordValue(value);
|
|
35
|
+
if (!candidate)
|
|
36
|
+
return null;
|
|
37
|
+
const name = normalizeCharacterExtractionText(candidate.name);
|
|
38
|
+
if (!name || name.length > CHARACTER_EXTRACTION_MAX_NAME_LENGTH)
|
|
39
|
+
return null;
|
|
40
|
+
const identity = normalizeCharacterExtractionText(candidate.identity);
|
|
41
|
+
const species = normalizeCharacterExtractionText(candidate.species);
|
|
42
|
+
const evidence = recordValue(candidate.firstEvidence);
|
|
43
|
+
const chapterId = normalizeCharacterExtractionText(evidence?.chapterId);
|
|
44
|
+
const chapterTitle = normalizeCharacterExtractionText(evidence?.chapterTitle);
|
|
45
|
+
const quote = normalizeCharacterExtractionText(evidence?.quote);
|
|
46
|
+
const stableCharacterId = normalizeCharacterExtractionText(candidate.stableCharacterId);
|
|
47
|
+
const candidateId = normalizeCharacterExtractionText(candidate.candidateId) || `candidate-${index + 1}`;
|
|
48
|
+
if (candidateId.length > 100 || !/^[A-Za-z0-9_-]+$/u.test(candidateId))
|
|
49
|
+
return null;
|
|
50
|
+
if (stableCharacterId.length > 200)
|
|
51
|
+
return null;
|
|
52
|
+
return {
|
|
53
|
+
candidateId,
|
|
54
|
+
name,
|
|
55
|
+
aliases: normalizeCharacterExtractionAliases(name, candidate.aliases),
|
|
56
|
+
species: species.length <= CHARACTER_EXTRACTION_MAX_SPECIES_LENGTH ? species : "",
|
|
57
|
+
identity: identity.length <= CHARACTER_EXTRACTION_MAX_IDENTITY_LENGTH ? identity : "",
|
|
58
|
+
firstChapterId: normalizeCharacterExtractionText(candidate.firstChapterId) || null,
|
|
59
|
+
firstEvidence: chapterId && quote && quote.length <= 80
|
|
60
|
+
? { chapterId, chapterTitle: chapterTitle.slice(0, 300), quote }
|
|
61
|
+
: null,
|
|
62
|
+
stableCharacterId: stableCharacterId || null
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function parseStoredCharacterExtractionCandidates(value) {
|
|
66
|
+
if (!Array.isArray(value) || value.length > CHARACTER_EXTRACTION_MAX_CANDIDATES) {
|
|
67
|
+
throw new AppError(409, "CHARACTER_EXTRACTION_PREVIEW_INVALID", "角色抽取预览数据无效,请重新运行任务");
|
|
68
|
+
}
|
|
69
|
+
const candidates = value.map((item, index) => normalizeCharacterExtractionCandidate(item, index));
|
|
70
|
+
if (candidates.some((candidate) => candidate === null)) {
|
|
71
|
+
throw new AppError(409, "CHARACTER_EXTRACTION_PREVIEW_INVALID", "角色抽取预览包含无效字段,请重新运行任务");
|
|
72
|
+
}
|
|
73
|
+
const parsed = candidates;
|
|
74
|
+
if (new Set(parsed.map((candidate) => candidate.candidateId)).size !== parsed.length) {
|
|
75
|
+
throw new AppError(409, "CHARACTER_EXTRACTION_PREVIEW_INVALID", "角色抽取预览候选标识重复,请重新运行任务");
|
|
76
|
+
}
|
|
77
|
+
return parsed;
|
|
78
|
+
}
|
|
79
|
+
export function editableCharacterExtractionCandidate(candidate, selection) {
|
|
80
|
+
const name = selection.name === undefined
|
|
81
|
+
? candidate.name
|
|
82
|
+
: normalizeCharacterExtractionText(selection.name);
|
|
83
|
+
if (!name || name.length > CHARACTER_EXTRACTION_MAX_NAME_LENGTH) {
|
|
84
|
+
throw new AppError(400, "CHARACTER_EXTRACTION_NAME_INVALID", "角色标准名不能为空且不能超过 200 个字符", {
|
|
85
|
+
candidateId: candidate.candidateId
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const aliases = selection.aliases === undefined
|
|
89
|
+
? candidate.aliases
|
|
90
|
+
: normalizeCharacterExtractionAliases(name, selection.aliases);
|
|
91
|
+
const rawAliases = selection.aliases ?? candidate.aliases;
|
|
92
|
+
const normalizedAliases = rawAliases.map((alias) => normalizeCharacterName(normalizeCharacterExtractionText(alias)));
|
|
93
|
+
if (rawAliases.length > CHARACTER_EXTRACTION_MAX_ALIASES
|
|
94
|
+
|| aliases.length !== rawAliases.length
|
|
95
|
+
|| normalizedAliases.some((alias) => !alias || alias === normalizeCharacterName(name))
|
|
96
|
+
|| new Set(normalizedAliases).size !== normalizedAliases.length) {
|
|
97
|
+
throw new AppError(400, "CHARACTER_EXTRACTION_ALIASES_INVALID", "角色别名不能为空、重复、等同标准名或超过长度限制", {
|
|
98
|
+
candidateId: candidate.candidateId
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const species = selection.species === undefined
|
|
102
|
+
? candidate.species
|
|
103
|
+
: normalizeCharacterExtractionText(selection.species);
|
|
104
|
+
const identity = selection.attributes?.identity === undefined
|
|
105
|
+
? candidate.identity
|
|
106
|
+
: normalizeCharacterExtractionText(selection.attributes.identity);
|
|
107
|
+
if (species.length > CHARACTER_EXTRACTION_MAX_SPECIES_LENGTH || identity.length > CHARACTER_EXTRACTION_MAX_IDENTITY_LENGTH) {
|
|
108
|
+
throw new AppError(400, "CHARACTER_EXTRACTION_ATTRIBUTES_INVALID", "角色种族或身份字段超过长度限制", {
|
|
109
|
+
candidateId: candidate.candidateId
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return { name, aliases, species, identity };
|
|
113
|
+
}
|
|
114
|
+
function canonicalValue(value) {
|
|
115
|
+
if (Array.isArray(value))
|
|
116
|
+
return value.map(canonicalValue);
|
|
117
|
+
const record = recordValue(value);
|
|
118
|
+
if (!record)
|
|
119
|
+
return value;
|
|
120
|
+
return Object.fromEntries(Object.keys(record).sort().map((key) => [key, canonicalValue(record[key])]));
|
|
121
|
+
}
|
|
122
|
+
export function characterExtractionHash(value) {
|
|
123
|
+
return createHash("sha256").update(JSON.stringify(canonicalValue(value))).digest("hex");
|
|
124
|
+
}
|
|
125
|
+
export function characterExtractionSelectionFingerprint(selections) {
|
|
126
|
+
return characterExtractionHash([...selections]
|
|
127
|
+
.map((selection) => ({
|
|
128
|
+
...selection,
|
|
129
|
+
aliases: selection.aliases ? [...selection.aliases] : undefined
|
|
130
|
+
}))
|
|
131
|
+
.sort((left, right) => left.candidateId.localeCompare(right.candidateId)));
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=character-extraction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"character-extraction.js","sourceRoot":"","sources":["../src/character-extraction.ts"],"names":[],"mappings":"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"}
|
package/dist/cli-core.js
CHANGED
|
@@ -450,7 +450,7 @@ function helpText() {
|
|
|
450
450
|
scriverse work get <workId>
|
|
451
451
|
scriverse work history <workId>
|
|
452
452
|
scriverse work restore <workId> --version <number>
|
|
453
|
-
scriverse manuscript get <workId> [--format json|markdown|txt|docx] [--output <path>]
|
|
453
|
+
scriverse manuscript get <workId> [--format json|markdown|txt|docx|epub] [--output <path>]
|
|
454
454
|
scriverse search <workId> --query <text> [--type <type>] [--limit <number>]
|
|
455
455
|
scriverse audit <workId>
|
|
456
456
|
scriverse writing progress <workId>
|
|
@@ -486,7 +486,7 @@ AI 编辑辅助:
|
|
|
486
486
|
}
|
|
487
487
|
function schemaList() {
|
|
488
488
|
return {
|
|
489
|
-
output: "除 manuscript 的 markdown/txt/docx 外,所有成功结果都输出 JSON;错误输出到 stderr 并返回非零状态码。",
|
|
489
|
+
output: "除 manuscript 的 markdown/txt/docx/epub 外,所有成功结果都输出 JSON;错误输出到 stderr 并返回非零状态码。",
|
|
490
490
|
input: {
|
|
491
491
|
json: "create/update 使用 --input file.json 或 --input - 从标准输入读取 JSON 对象",
|
|
492
492
|
longText: "使用可重复的 --field-file field=path 注入长文本字段",
|
|
@@ -715,15 +715,16 @@ async function execute(parsed, dependencies) {
|
|
|
715
715
|
const workId = requiredPosition(parsed.positionals, 2, "workId");
|
|
716
716
|
assertPositionCount(parsed.positionals, 3);
|
|
717
717
|
const format = option(parsed, "format") ?? "json";
|
|
718
|
-
if (!["json", "markdown", "txt", "docx"].includes(format))
|
|
719
|
-
throw new CliError("CLI_FORMAT_INVALID", "format 必须是 json、markdown、txt 或
|
|
718
|
+
if (!["json", "markdown", "txt", "docx", "epub"].includes(format))
|
|
719
|
+
throw new CliError("CLI_FORMAT_INVALID", "format 必须是 json、markdown、txt、docx 或 epub");
|
|
720
720
|
if (format === "json") {
|
|
721
721
|
if (option(parsed, "output"))
|
|
722
722
|
throw new CliError("CLI_OUTPUT_UNSUPPORTED", "JSON 格式直接输出到标准输出,不支持 --output");
|
|
723
723
|
emitJson(dependencies.stdout, await apiRequest(dependencies.fetchImpl, config, `/api/works/${encoded(workId)}`), compact);
|
|
724
724
|
}
|
|
725
|
-
else if (format === "markdown" || format === "docx") {
|
|
726
|
-
const
|
|
725
|
+
else if (format === "markdown" || format === "docx" || format === "epub") {
|
|
726
|
+
const defaultOutput = format === "docx" ? `novel-${workId}.docx` : format === "epub" ? `novel-${workId}.epub` : `novel-${workId}.zip`;
|
|
727
|
+
const outputPath = option(parsed, "output") ?? defaultOutput;
|
|
727
728
|
emitJson(dependencies.stdout, {
|
|
728
729
|
format,
|
|
729
730
|
...(await downloadToFile(dependencies.fetchImpl, config, `/api/works/${encoded(workId)}/export?format=${format}`, outputPath, dependencies.cwd))
|