gstinfo 0.0.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.
- package/bun.lock +20 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +453 -0
- package/package.json +13 -0
- package/src/index.ts +602 -0
- package/tsconfig.json +15 -0
package/bun.lock
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "gstinfo",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/node": "^25.5.0",
|
|
9
|
+
"typescript": "^5.2.0",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
"packages": {
|
|
14
|
+
"@types/node": ["@types/node@25.5.0", "https://registry.npmmirror.com/@types/node/-/node-25.5.0.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
|
15
|
+
|
|
16
|
+
"typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
17
|
+
|
|
18
|
+
"undici-types": ["undici-types@7.18.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type FileInput = string | Uint8Array | ArrayBuffer;
|
|
2
|
+
export type JsonObject = Record<string, unknown>;
|
|
3
|
+
export interface WorldEntryInfo {
|
|
4
|
+
raw: JsonObject;
|
|
5
|
+
uid: number | string | undefined;
|
|
6
|
+
keys: string[];
|
|
7
|
+
secondaryKeys: string[];
|
|
8
|
+
comment: string;
|
|
9
|
+
content: string;
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
order: number | undefined;
|
|
12
|
+
position: number | string | undefined;
|
|
13
|
+
probability: number | undefined;
|
|
14
|
+
depth: number | undefined;
|
|
15
|
+
}
|
|
16
|
+
export interface WorldInfo {
|
|
17
|
+
raw: JsonObject;
|
|
18
|
+
name: string;
|
|
19
|
+
entries: WorldEntryInfo[];
|
|
20
|
+
entryCount: number;
|
|
21
|
+
}
|
|
22
|
+
export interface CharacterInfo {
|
|
23
|
+
raw: JsonObject;
|
|
24
|
+
data: JsonObject | null;
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
personality: string;
|
|
28
|
+
scenario: string;
|
|
29
|
+
firstMessage: string;
|
|
30
|
+
exampleMessages: string;
|
|
31
|
+
tags: string[];
|
|
32
|
+
creatorComment: string;
|
|
33
|
+
avatar: string;
|
|
34
|
+
spec: string;
|
|
35
|
+
specVersion: string;
|
|
36
|
+
worldInfo: WorldInfo | null;
|
|
37
|
+
}
|
|
38
|
+
export interface PresetsInfo {
|
|
39
|
+
raw: JsonObject;
|
|
40
|
+
source: string;
|
|
41
|
+
model: string;
|
|
42
|
+
temperature: number | undefined;
|
|
43
|
+
topP: number | undefined;
|
|
44
|
+
topK: number | undefined;
|
|
45
|
+
minP: number | undefined;
|
|
46
|
+
maxContext: number | undefined;
|
|
47
|
+
maxTokens: number | undefined;
|
|
48
|
+
seed: number | undefined;
|
|
49
|
+
stream: boolean | undefined;
|
|
50
|
+
}
|
|
51
|
+
export type PathSegment = string | number;
|
|
52
|
+
export type PathInput = string | PathSegment[];
|
|
53
|
+
export declare function getValueByPath<T = unknown>(source: unknown, path: PathInput, defaultValue?: T): T | undefined;
|
|
54
|
+
export declare function getCharacterInfo(input: FileInput): Promise<CharacterInfo>;
|
|
55
|
+
export declare function getWorldInfo(input: FileInput): Promise<WorldInfo>;
|
|
56
|
+
export declare function getPresetsInfo(input: FileInput): Promise<PresetsInfo>;
|
|
57
|
+
export declare function isCharacterInfo(value: unknown): value is CharacterInfo;
|
|
58
|
+
export declare function isWorldInfo(value: unknown): value is WorldInfo;
|
|
59
|
+
export declare function isPresetsInfo(value: unknown): value is PresetsInfo;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { inflateSync } from "node:zlib";
|
|
3
|
+
const PNG_SIGNATURE = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
4
|
+
const textDecoder = new TextDecoder("utf-8");
|
|
5
|
+
function asUint8Array(input) {
|
|
6
|
+
if (typeof input === "string") {
|
|
7
|
+
throw new Error("不支持将字符串直接转换为二进制,请先读取文件");
|
|
8
|
+
}
|
|
9
|
+
if (input instanceof Uint8Array) {
|
|
10
|
+
return input;
|
|
11
|
+
}
|
|
12
|
+
return new Uint8Array(input);
|
|
13
|
+
}
|
|
14
|
+
async function readInputBytes(input) {
|
|
15
|
+
if (typeof input === "string") {
|
|
16
|
+
const buffer = await readFile(input);
|
|
17
|
+
return new Uint8Array(buffer);
|
|
18
|
+
}
|
|
19
|
+
return asUint8Array(input);
|
|
20
|
+
}
|
|
21
|
+
function bufferStartsWith(buffer, signature) {
|
|
22
|
+
if (buffer.length < signature.length) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
for (let i = 0; i < signature.length; i += 1) {
|
|
26
|
+
if (buffer[i] !== signature[i]) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
function findNullByteIndex(buffer, start) {
|
|
33
|
+
for (let i = start; i < buffer.length; i += 1) {
|
|
34
|
+
if (buffer[i] === 0) {
|
|
35
|
+
return i;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return -1;
|
|
39
|
+
}
|
|
40
|
+
function parseJsonCandidate(value) {
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
if (!trimmed) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(trimmed);
|
|
47
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
return { value: parsed };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function parseBase64JsonCandidate(value) {
|
|
57
|
+
const trimmed = value.trim();
|
|
58
|
+
if (!trimmed) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
let normalized = trimmed.replace(/\s+/g, "");
|
|
62
|
+
if (!/^[A-Za-z0-9+/=_-]+$/.test(normalized)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
normalized = normalized.replace(/-/g, "+").replace(/_/g, "/");
|
|
66
|
+
const remainder = normalized.length % 4;
|
|
67
|
+
if (remainder === 2) {
|
|
68
|
+
normalized += "==";
|
|
69
|
+
}
|
|
70
|
+
else if (remainder === 3) {
|
|
71
|
+
normalized += "=";
|
|
72
|
+
}
|
|
73
|
+
else if (remainder === 1) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const decoded = Buffer.from(normalized, "base64").toString("utf-8");
|
|
78
|
+
return parseJsonCandidate(decoded);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function parseTextPayload(payload) {
|
|
85
|
+
const separator = findNullByteIndex(payload, 0);
|
|
86
|
+
if (separator < 0 || separator + 1 >= payload.length) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return textDecoder.decode(payload.subarray(separator + 1));
|
|
90
|
+
}
|
|
91
|
+
function parseZtxtPayload(payload) {
|
|
92
|
+
const separator = findNullByteIndex(payload, 0);
|
|
93
|
+
if (separator < 0 || separator + 2 > payload.length) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const compressionMethod = payload[separator + 1];
|
|
97
|
+
if (compressionMethod !== 0) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const decompressed = inflateSync(payload.subarray(separator + 2));
|
|
102
|
+
return textDecoder.decode(decompressed);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function parseItxtPayload(payload) {
|
|
109
|
+
const keywordEnd = findNullByteIndex(payload, 0);
|
|
110
|
+
if (keywordEnd < 0 || keywordEnd + 5 > payload.length) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const compressionFlag = payload[keywordEnd + 1];
|
|
114
|
+
const compressionMethod = payload[keywordEnd + 2];
|
|
115
|
+
let cursor = keywordEnd + 3;
|
|
116
|
+
const languageTagEnd = findNullByteIndex(payload, cursor);
|
|
117
|
+
if (languageTagEnd < 0) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
cursor = languageTagEnd + 1;
|
|
121
|
+
const translatedKeywordEnd = findNullByteIndex(payload, cursor);
|
|
122
|
+
if (translatedKeywordEnd < 0) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
cursor = translatedKeywordEnd + 1;
|
|
126
|
+
const textData = payload.subarray(cursor);
|
|
127
|
+
if (compressionFlag === 1) {
|
|
128
|
+
if (compressionMethod !== 0) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
return textDecoder.decode(inflateSync(textData));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return textDecoder.decode(textData);
|
|
139
|
+
}
|
|
140
|
+
function extractTextChunksFromPng(bytes) {
|
|
141
|
+
if (!bufferStartsWith(bytes, PNG_SIGNATURE)) {
|
|
142
|
+
throw new Error("文件不是有效的 PNG");
|
|
143
|
+
}
|
|
144
|
+
const texts = [];
|
|
145
|
+
let offset = PNG_SIGNATURE.length;
|
|
146
|
+
while (offset + 12 <= bytes.length) {
|
|
147
|
+
const chunkLength = (bytes[offset] << 24) |
|
|
148
|
+
(bytes[offset + 1] << 16) |
|
|
149
|
+
(bytes[offset + 2] << 8) |
|
|
150
|
+
bytes[offset + 3];
|
|
151
|
+
const type = String.fromCharCode(bytes[offset + 4], bytes[offset + 5], bytes[offset + 6], bytes[offset + 7]);
|
|
152
|
+
const dataStart = offset + 8;
|
|
153
|
+
const dataEnd = dataStart + chunkLength;
|
|
154
|
+
const crcEnd = dataEnd + 4;
|
|
155
|
+
if (chunkLength < 0 || crcEnd > bytes.length) {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
const payload = bytes.subarray(dataStart, dataEnd);
|
|
159
|
+
let parsed = null;
|
|
160
|
+
if (type === "tEXt") {
|
|
161
|
+
parsed = parseTextPayload(payload);
|
|
162
|
+
}
|
|
163
|
+
else if (type === "zTXt") {
|
|
164
|
+
parsed = parseZtxtPayload(payload);
|
|
165
|
+
}
|
|
166
|
+
else if (type === "iTXt") {
|
|
167
|
+
parsed = parseItxtPayload(payload);
|
|
168
|
+
}
|
|
169
|
+
if (parsed) {
|
|
170
|
+
texts.push(parsed);
|
|
171
|
+
}
|
|
172
|
+
offset = crcEnd;
|
|
173
|
+
if (type === "IEND") {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return texts;
|
|
178
|
+
}
|
|
179
|
+
function parseJsonFromPng(bytes) {
|
|
180
|
+
const textChunks = extractTextChunksFromPng(bytes);
|
|
181
|
+
const keywordPriority = ["chara", "ccv3", "character", "card"];
|
|
182
|
+
const prioritized = [...textChunks].sort((a, b) => {
|
|
183
|
+
const ai = keywordPriority.findIndex((k) => a.toLowerCase().includes(k));
|
|
184
|
+
const bi = keywordPriority.findIndex((k) => b.toLowerCase().includes(k));
|
|
185
|
+
const aa = ai < 0 ? Number.MAX_SAFE_INTEGER : ai;
|
|
186
|
+
const bb = bi < 0 ? Number.MAX_SAFE_INTEGER : bi;
|
|
187
|
+
return aa - bb;
|
|
188
|
+
});
|
|
189
|
+
for (const value of prioritized) {
|
|
190
|
+
const direct = parseJsonCandidate(value);
|
|
191
|
+
if (direct) {
|
|
192
|
+
return direct;
|
|
193
|
+
}
|
|
194
|
+
const decoded = parseBase64JsonCandidate(value);
|
|
195
|
+
if (decoded) {
|
|
196
|
+
return decoded;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
throw new Error("未在 PNG 中找到可解析的角色卡 JSON");
|
|
200
|
+
}
|
|
201
|
+
function parseJsonFromText(text) {
|
|
202
|
+
const direct = parseJsonCandidate(text);
|
|
203
|
+
if (direct) {
|
|
204
|
+
return direct;
|
|
205
|
+
}
|
|
206
|
+
const decoded = parseBase64JsonCandidate(text);
|
|
207
|
+
if (decoded) {
|
|
208
|
+
return decoded;
|
|
209
|
+
}
|
|
210
|
+
throw new Error("文件内容不是有效 JSON");
|
|
211
|
+
}
|
|
212
|
+
async function parseAnyJson(input) {
|
|
213
|
+
const bytes = await readInputBytes(input);
|
|
214
|
+
if (bufferStartsWith(bytes, PNG_SIGNATURE)) {
|
|
215
|
+
return parseJsonFromPng(bytes);
|
|
216
|
+
}
|
|
217
|
+
return parseJsonFromText(textDecoder.decode(bytes));
|
|
218
|
+
}
|
|
219
|
+
function asObject(value) {
|
|
220
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function asString(value) {
|
|
226
|
+
return typeof value === "string" ? value : "";
|
|
227
|
+
}
|
|
228
|
+
function asStringArray(value) {
|
|
229
|
+
if (!Array.isArray(value)) {
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
return value.filter((item) => typeof item === "string");
|
|
233
|
+
}
|
|
234
|
+
function asNumber(value) {
|
|
235
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
236
|
+
}
|
|
237
|
+
function asBoolean(value) {
|
|
238
|
+
return typeof value === "boolean" ? value : undefined;
|
|
239
|
+
}
|
|
240
|
+
function isJsonObjectValue(value) {
|
|
241
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
242
|
+
}
|
|
243
|
+
function parsePath(path) {
|
|
244
|
+
if (Array.isArray(path)) {
|
|
245
|
+
return path;
|
|
246
|
+
}
|
|
247
|
+
const normalized = path.replace(/\[(\d+)\]/g, ".$1");
|
|
248
|
+
return normalized
|
|
249
|
+
.split(".")
|
|
250
|
+
.map((segment) => segment.trim())
|
|
251
|
+
.filter((segment) => segment.length > 0)
|
|
252
|
+
.map((segment) => (/^\d+$/.test(segment) ? Number(segment) : segment));
|
|
253
|
+
}
|
|
254
|
+
export function getValueByPath(source, path, defaultValue) {
|
|
255
|
+
const segments = parsePath(path);
|
|
256
|
+
if (segments.length === 0) {
|
|
257
|
+
if (source === undefined) {
|
|
258
|
+
return defaultValue;
|
|
259
|
+
}
|
|
260
|
+
return source;
|
|
261
|
+
}
|
|
262
|
+
let current = source;
|
|
263
|
+
for (const segment of segments) {
|
|
264
|
+
if (typeof segment === "number") {
|
|
265
|
+
if (!Array.isArray(current) || segment < 0 || segment >= current.length) {
|
|
266
|
+
return defaultValue;
|
|
267
|
+
}
|
|
268
|
+
current = current[segment];
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!isJsonObjectValue(current) || !(segment in current)) {
|
|
272
|
+
return defaultValue;
|
|
273
|
+
}
|
|
274
|
+
current = current[segment];
|
|
275
|
+
}
|
|
276
|
+
if (current === undefined) {
|
|
277
|
+
return defaultValue;
|
|
278
|
+
}
|
|
279
|
+
return current;
|
|
280
|
+
}
|
|
281
|
+
function pickString(source, keys) {
|
|
282
|
+
if (!source) {
|
|
283
|
+
return "";
|
|
284
|
+
}
|
|
285
|
+
for (const key of keys) {
|
|
286
|
+
const value = source[key];
|
|
287
|
+
if (typeof value === "string" && value.length > 0) {
|
|
288
|
+
return value;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return "";
|
|
292
|
+
}
|
|
293
|
+
function extractWorldRawFromParsed(parsed) {
|
|
294
|
+
const directKeys = ["character_book", "worldbook", "world_info", "lorebook"];
|
|
295
|
+
for (const key of directKeys) {
|
|
296
|
+
const candidate = asObject(parsed[key]);
|
|
297
|
+
if (candidate) {
|
|
298
|
+
return candidate;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const data = asObject(parsed.data);
|
|
302
|
+
if (data) {
|
|
303
|
+
for (const key of directKeys) {
|
|
304
|
+
const candidate = asObject(data[key]);
|
|
305
|
+
if (candidate) {
|
|
306
|
+
return candidate;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const extensions = data ? asObject(data.extensions) : asObject(parsed.extensions);
|
|
311
|
+
if (extensions) {
|
|
312
|
+
const extensionKeys = ["world", "worldbook", "character_book", "lorebook"];
|
|
313
|
+
for (const key of extensionKeys) {
|
|
314
|
+
const candidate = asObject(extensions[key]);
|
|
315
|
+
if (candidate) {
|
|
316
|
+
return candidate;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return parsed;
|
|
321
|
+
}
|
|
322
|
+
function normalizeWorldEntry(entry) {
|
|
323
|
+
return {
|
|
324
|
+
raw: entry,
|
|
325
|
+
uid: typeof entry.uid === "number" || typeof entry.uid === "string" ? entry.uid : undefined,
|
|
326
|
+
keys: asStringArray(entry.keys),
|
|
327
|
+
secondaryKeys: asStringArray(entry.secondary_keys),
|
|
328
|
+
comment: asString(entry.comment),
|
|
329
|
+
content: asString(entry.content),
|
|
330
|
+
enabled: entry.enabled !== false,
|
|
331
|
+
order: asNumber(entry.order),
|
|
332
|
+
position: typeof entry.position === "number" || typeof entry.position === "string"
|
|
333
|
+
? entry.position
|
|
334
|
+
: undefined,
|
|
335
|
+
probability: asNumber(entry.probability),
|
|
336
|
+
depth: asNumber(entry.depth),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
function createWorldInfo(raw) {
|
|
340
|
+
const entriesRaw = Array.isArray(raw.entries) ? raw.entries : [];
|
|
341
|
+
const entries = entriesRaw
|
|
342
|
+
.map((entry) => asObject(entry))
|
|
343
|
+
.filter((entry) => entry !== null)
|
|
344
|
+
.map((entry) => normalizeWorldEntry(entry));
|
|
345
|
+
return {
|
|
346
|
+
raw,
|
|
347
|
+
name: pickString(raw, ["name", "title", "world_name"]),
|
|
348
|
+
entries,
|
|
349
|
+
entryCount: entries.length,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function createCharacterInfo(raw) {
|
|
353
|
+
const data = asObject(raw.data);
|
|
354
|
+
const worldRaw = extractWorldRawFromParsed(raw);
|
|
355
|
+
const worldEntries = Array.isArray(worldRaw.entries) ? worldRaw.entries : [];
|
|
356
|
+
const worldInfo = worldEntries.length > 0 ? createWorldInfo(worldRaw) : null;
|
|
357
|
+
const tagsFromData = asStringArray(data?.tags);
|
|
358
|
+
const tags = tagsFromData.length > 0 ? tagsFromData : asStringArray(raw.tags);
|
|
359
|
+
return {
|
|
360
|
+
raw,
|
|
361
|
+
data,
|
|
362
|
+
name: pickString(data, ["name"]) || pickString(raw, ["name"]),
|
|
363
|
+
description: pickString(data, ["description"]) || pickString(raw, ["description"]),
|
|
364
|
+
personality: pickString(data, ["personality"]) || pickString(raw, ["personality"]),
|
|
365
|
+
scenario: pickString(data, ["scenario"]) || pickString(raw, ["scenario"]),
|
|
366
|
+
firstMessage: pickString(data, ["first_mes", "firstMessage"]) || pickString(raw, ["first_mes", "firstMessage"]),
|
|
367
|
+
exampleMessages: pickString(data, ["mes_example", "example_messages"]) || pickString(raw, ["mes_example", "example_messages"]),
|
|
368
|
+
tags,
|
|
369
|
+
creatorComment: pickString(data, ["creator_notes", "creatorcomment"]) || pickString(raw, ["creator_notes", "creatorcomment"]),
|
|
370
|
+
avatar: pickString(data, ["avatar"]) || pickString(raw, ["avatar"]),
|
|
371
|
+
spec: pickString(raw, ["spec"]),
|
|
372
|
+
specVersion: pickString(raw, ["spec_version"]),
|
|
373
|
+
worldInfo,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function resolvePresetModel(raw, source) {
|
|
377
|
+
const bySource = {
|
|
378
|
+
openai: "openai_model",
|
|
379
|
+
claude: "claude_model",
|
|
380
|
+
windowai: "windowai_model",
|
|
381
|
+
openrouter: "openrouter_model",
|
|
382
|
+
ai21: "ai21_model",
|
|
383
|
+
mistralai: "mistralai_model",
|
|
384
|
+
cohere: "cohere_model",
|
|
385
|
+
perplexity: "perplexity_model",
|
|
386
|
+
groq: "groq_model",
|
|
387
|
+
zerooneai: "zerooneai_model",
|
|
388
|
+
blockentropy: "blockentropy_model",
|
|
389
|
+
custom: "custom_model",
|
|
390
|
+
google: "google_model",
|
|
391
|
+
};
|
|
392
|
+
const modelKey = bySource[source];
|
|
393
|
+
if (modelKey && typeof raw[modelKey] === "string") {
|
|
394
|
+
return asString(raw[modelKey]);
|
|
395
|
+
}
|
|
396
|
+
const fallbackKeys = Object.values(bySource);
|
|
397
|
+
for (const key of fallbackKeys) {
|
|
398
|
+
if (typeof raw[key] === "string" && asString(raw[key]).length > 0) {
|
|
399
|
+
return asString(raw[key]);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return "";
|
|
403
|
+
}
|
|
404
|
+
function createPresetsInfo(raw) {
|
|
405
|
+
const source = pickString(raw, ["chat_completion_source"]);
|
|
406
|
+
return {
|
|
407
|
+
raw,
|
|
408
|
+
source,
|
|
409
|
+
model: resolvePresetModel(raw, source),
|
|
410
|
+
temperature: asNumber(raw.temperature),
|
|
411
|
+
topP: asNumber(raw.top_p),
|
|
412
|
+
topK: asNumber(raw.top_k),
|
|
413
|
+
minP: asNumber(raw.min_p),
|
|
414
|
+
maxContext: asNumber(raw.openai_max_context),
|
|
415
|
+
maxTokens: asNumber(raw.openai_max_tokens),
|
|
416
|
+
seed: asNumber(raw.seed),
|
|
417
|
+
stream: asBoolean(raw.stream_openai),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
export async function getCharacterInfo(input) {
|
|
421
|
+
const raw = await parseAnyJson(input);
|
|
422
|
+
return createCharacterInfo(raw);
|
|
423
|
+
}
|
|
424
|
+
export async function getWorldInfo(input) {
|
|
425
|
+
const parsed = await parseAnyJson(input);
|
|
426
|
+
const worldRaw = extractWorldRawFromParsed(parsed);
|
|
427
|
+
return createWorldInfo(worldRaw);
|
|
428
|
+
}
|
|
429
|
+
export async function getPresetsInfo(input) {
|
|
430
|
+
const raw = await parseAnyJson(input);
|
|
431
|
+
return createPresetsInfo(raw);
|
|
432
|
+
}
|
|
433
|
+
export function isCharacterInfo(value) {
|
|
434
|
+
if (!isJsonObjectValue(value)) {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
return (isJsonObjectValue(value.raw) &&
|
|
438
|
+
typeof value.name === "string" &&
|
|
439
|
+
Array.isArray(value.tags) &&
|
|
440
|
+
"worldInfo" in value);
|
|
441
|
+
}
|
|
442
|
+
export function isWorldInfo(value) {
|
|
443
|
+
if (!isJsonObjectValue(value)) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
return isJsonObjectValue(value.raw) && typeof value.name === "string" && Array.isArray(value.entries);
|
|
447
|
+
}
|
|
448
|
+
export function isPresetsInfo(value) {
|
|
449
|
+
if (!isJsonObjectValue(value)) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
return isJsonObjectValue(value.raw) && typeof value.source === "string" && typeof value.model === "string";
|
|
453
|
+
}
|
package/package.json
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { inflateSync } from "node:zlib";
|
|
3
|
+
|
|
4
|
+
export type FileInput = string | Uint8Array | ArrayBuffer;
|
|
5
|
+
export type JsonObject = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
export interface WorldEntryInfo {
|
|
8
|
+
raw: JsonObject;
|
|
9
|
+
uid: number | string | undefined;
|
|
10
|
+
keys: string[];
|
|
11
|
+
secondaryKeys: string[];
|
|
12
|
+
comment: string;
|
|
13
|
+
content: string;
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
order: number | undefined;
|
|
16
|
+
position: number | string | undefined;
|
|
17
|
+
probability: number | undefined;
|
|
18
|
+
depth: number | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WorldInfo {
|
|
22
|
+
raw: JsonObject;
|
|
23
|
+
name: string;
|
|
24
|
+
entries: WorldEntryInfo[];
|
|
25
|
+
entryCount: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CharacterInfo {
|
|
29
|
+
raw: JsonObject;
|
|
30
|
+
data: JsonObject | null;
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
personality: string;
|
|
34
|
+
scenario: string;
|
|
35
|
+
firstMessage: string;
|
|
36
|
+
exampleMessages: string;
|
|
37
|
+
tags: string[];
|
|
38
|
+
creatorComment: string;
|
|
39
|
+
avatar: string;
|
|
40
|
+
spec: string;
|
|
41
|
+
specVersion: string;
|
|
42
|
+
worldInfo: WorldInfo | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PresetsInfo {
|
|
46
|
+
raw: JsonObject;
|
|
47
|
+
source: string;
|
|
48
|
+
model: string;
|
|
49
|
+
temperature: number | undefined;
|
|
50
|
+
topP: number | undefined;
|
|
51
|
+
topK: number | undefined;
|
|
52
|
+
minP: number | undefined;
|
|
53
|
+
maxContext: number | undefined;
|
|
54
|
+
maxTokens: number | undefined;
|
|
55
|
+
seed: number | undefined;
|
|
56
|
+
stream: boolean | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type PathSegment = string | number;
|
|
60
|
+
export type PathInput = string | PathSegment[];
|
|
61
|
+
|
|
62
|
+
const PNG_SIGNATURE = Uint8Array.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
63
|
+
const textDecoder = new TextDecoder("utf-8");
|
|
64
|
+
|
|
65
|
+
function asUint8Array(input: FileInput): Uint8Array {
|
|
66
|
+
if (typeof input === "string") {
|
|
67
|
+
throw new Error("不支持将字符串直接转换为二进制,请先读取文件");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (input instanceof Uint8Array) {
|
|
71
|
+
return input;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return new Uint8Array(input);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function readInputBytes(input: FileInput): Promise<Uint8Array> {
|
|
78
|
+
if (typeof input === "string") {
|
|
79
|
+
const buffer = await readFile(input);
|
|
80
|
+
return new Uint8Array(buffer);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return asUint8Array(input);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function bufferStartsWith(buffer: Uint8Array, signature: Uint8Array): boolean {
|
|
87
|
+
if (buffer.length < signature.length) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < signature.length; i += 1) {
|
|
92
|
+
if (buffer[i] !== signature[i]) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function findNullByteIndex(buffer: Uint8Array, start: number): number {
|
|
101
|
+
for (let i = start; i < buffer.length; i += 1) {
|
|
102
|
+
if (buffer[i] === 0) {
|
|
103
|
+
return i;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return -1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseJsonCandidate(value: string): JsonObject | null {
|
|
111
|
+
const trimmed = value.trim();
|
|
112
|
+
if (!trimmed) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(trimmed);
|
|
118
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
119
|
+
return parsed as JsonObject;
|
|
120
|
+
}
|
|
121
|
+
return { value: parsed };
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseBase64JsonCandidate(value: string): JsonObject | null {
|
|
128
|
+
const trimmed = value.trim();
|
|
129
|
+
if (!trimmed) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let normalized = trimmed.replace(/\s+/g, "");
|
|
134
|
+
if (!/^[A-Za-z0-9+/=_-]+$/.test(normalized)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
normalized = normalized.replace(/-/g, "+").replace(/_/g, "/");
|
|
139
|
+
const remainder = normalized.length % 4;
|
|
140
|
+
if (remainder === 2) {
|
|
141
|
+
normalized += "==";
|
|
142
|
+
} else if (remainder === 3) {
|
|
143
|
+
normalized += "=";
|
|
144
|
+
} else if (remainder === 1) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const decoded = Buffer.from(normalized, "base64").toString("utf-8");
|
|
150
|
+
return parseJsonCandidate(decoded);
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseTextPayload(payload: Uint8Array): string | null {
|
|
157
|
+
const separator = findNullByteIndex(payload, 0);
|
|
158
|
+
if (separator < 0 || separator + 1 >= payload.length) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return textDecoder.decode(payload.subarray(separator + 1));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseZtxtPayload(payload: Uint8Array): string | null {
|
|
166
|
+
const separator = findNullByteIndex(payload, 0);
|
|
167
|
+
if (separator < 0 || separator + 2 > payload.length) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const compressionMethod = payload[separator + 1];
|
|
172
|
+
if (compressionMethod !== 0) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const decompressed = inflateSync(payload.subarray(separator + 2));
|
|
178
|
+
return textDecoder.decode(decompressed);
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseItxtPayload(payload: Uint8Array): string | null {
|
|
185
|
+
const keywordEnd = findNullByteIndex(payload, 0);
|
|
186
|
+
if (keywordEnd < 0 || keywordEnd + 5 > payload.length) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const compressionFlag = payload[keywordEnd + 1];
|
|
191
|
+
const compressionMethod = payload[keywordEnd + 2];
|
|
192
|
+
let cursor = keywordEnd + 3;
|
|
193
|
+
|
|
194
|
+
const languageTagEnd = findNullByteIndex(payload, cursor);
|
|
195
|
+
if (languageTagEnd < 0) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
cursor = languageTagEnd + 1;
|
|
199
|
+
|
|
200
|
+
const translatedKeywordEnd = findNullByteIndex(payload, cursor);
|
|
201
|
+
if (translatedKeywordEnd < 0) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
cursor = translatedKeywordEnd + 1;
|
|
205
|
+
|
|
206
|
+
const textData = payload.subarray(cursor);
|
|
207
|
+
if (compressionFlag === 1) {
|
|
208
|
+
if (compressionMethod !== 0) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
return textDecoder.decode(inflateSync(textData));
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return textDecoder.decode(textData);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function extractTextChunksFromPng(bytes: Uint8Array): string[] {
|
|
222
|
+
if (!bufferStartsWith(bytes, PNG_SIGNATURE)) {
|
|
223
|
+
throw new Error("文件不是有效的 PNG");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const texts: string[] = [];
|
|
227
|
+
let offset = PNG_SIGNATURE.length;
|
|
228
|
+
|
|
229
|
+
while (offset + 12 <= bytes.length) {
|
|
230
|
+
const chunkLength =
|
|
231
|
+
(bytes[offset] << 24) |
|
|
232
|
+
(bytes[offset + 1] << 16) |
|
|
233
|
+
(bytes[offset + 2] << 8) |
|
|
234
|
+
bytes[offset + 3];
|
|
235
|
+
const type = String.fromCharCode(
|
|
236
|
+
bytes[offset + 4],
|
|
237
|
+
bytes[offset + 5],
|
|
238
|
+
bytes[offset + 6],
|
|
239
|
+
bytes[offset + 7]
|
|
240
|
+
);
|
|
241
|
+
const dataStart = offset + 8;
|
|
242
|
+
const dataEnd = dataStart + chunkLength;
|
|
243
|
+
const crcEnd = dataEnd + 4;
|
|
244
|
+
|
|
245
|
+
if (chunkLength < 0 || crcEnd > bytes.length) {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const payload = bytes.subarray(dataStart, dataEnd);
|
|
250
|
+
let parsed: string | null = null;
|
|
251
|
+
|
|
252
|
+
if (type === "tEXt") {
|
|
253
|
+
parsed = parseTextPayload(payload);
|
|
254
|
+
} else if (type === "zTXt") {
|
|
255
|
+
parsed = parseZtxtPayload(payload);
|
|
256
|
+
} else if (type === "iTXt") {
|
|
257
|
+
parsed = parseItxtPayload(payload);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (parsed) {
|
|
261
|
+
texts.push(parsed);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
offset = crcEnd;
|
|
265
|
+
if (type === "IEND") {
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return texts;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function parseJsonFromPng(bytes: Uint8Array): JsonObject {
|
|
274
|
+
const textChunks = extractTextChunksFromPng(bytes);
|
|
275
|
+
const keywordPriority = ["chara", "ccv3", "character", "card"];
|
|
276
|
+
|
|
277
|
+
const prioritized = [...textChunks].sort((a, b) => {
|
|
278
|
+
const ai = keywordPriority.findIndex((k) => a.toLowerCase().includes(k));
|
|
279
|
+
const bi = keywordPriority.findIndex((k) => b.toLowerCase().includes(k));
|
|
280
|
+
const aa = ai < 0 ? Number.MAX_SAFE_INTEGER : ai;
|
|
281
|
+
const bb = bi < 0 ? Number.MAX_SAFE_INTEGER : bi;
|
|
282
|
+
return aa - bb;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
for (const value of prioritized) {
|
|
286
|
+
const direct = parseJsonCandidate(value);
|
|
287
|
+
if (direct) {
|
|
288
|
+
return direct;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const decoded = parseBase64JsonCandidate(value);
|
|
292
|
+
if (decoded) {
|
|
293
|
+
return decoded;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
throw new Error("未在 PNG 中找到可解析的角色卡 JSON");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function parseJsonFromText(text: string): JsonObject {
|
|
301
|
+
const direct = parseJsonCandidate(text);
|
|
302
|
+
if (direct) {
|
|
303
|
+
return direct;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const decoded = parseBase64JsonCandidate(text);
|
|
307
|
+
if (decoded) {
|
|
308
|
+
return decoded;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
throw new Error("文件内容不是有效 JSON");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function parseAnyJson(input: FileInput): Promise<JsonObject> {
|
|
315
|
+
const bytes = await readInputBytes(input);
|
|
316
|
+
if (bufferStartsWith(bytes, PNG_SIGNATURE)) {
|
|
317
|
+
return parseJsonFromPng(bytes);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return parseJsonFromText(textDecoder.decode(bytes));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function asObject(value: unknown): JsonObject | null {
|
|
324
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
325
|
+
return value as JsonObject;
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function asString(value: unknown): string {
|
|
331
|
+
return typeof value === "string" ? value : "";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function asStringArray(value: unknown): string[] {
|
|
335
|
+
if (!Array.isArray(value)) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
return value.filter((item): item is string => typeof item === "string");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function asNumber(value: unknown): number | undefined {
|
|
342
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function asBoolean(value: unknown): boolean | undefined {
|
|
346
|
+
return typeof value === "boolean" ? value : undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function isJsonObjectValue(value: unknown): value is JsonObject {
|
|
350
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function parsePath(path: PathInput): PathSegment[] {
|
|
354
|
+
if (Array.isArray(path)) {
|
|
355
|
+
return path;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const normalized = path.replace(/\[(\d+)\]/g, ".$1");
|
|
359
|
+
return normalized
|
|
360
|
+
.split(".")
|
|
361
|
+
.map((segment) => segment.trim())
|
|
362
|
+
.filter((segment) => segment.length > 0)
|
|
363
|
+
.map((segment) => (/^\d+$/.test(segment) ? Number(segment) : segment));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function getValueByPath<T = unknown>(
|
|
367
|
+
source: unknown,
|
|
368
|
+
path: PathInput,
|
|
369
|
+
defaultValue?: T
|
|
370
|
+
): T | undefined {
|
|
371
|
+
const segments = parsePath(path);
|
|
372
|
+
if (segments.length === 0) {
|
|
373
|
+
if (source === undefined) {
|
|
374
|
+
return defaultValue;
|
|
375
|
+
}
|
|
376
|
+
return source as T;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let current: unknown = source;
|
|
380
|
+
for (const segment of segments) {
|
|
381
|
+
if (typeof segment === "number") {
|
|
382
|
+
if (!Array.isArray(current) || segment < 0 || segment >= current.length) {
|
|
383
|
+
return defaultValue;
|
|
384
|
+
}
|
|
385
|
+
current = current[segment];
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!isJsonObjectValue(current) || !(segment in current)) {
|
|
390
|
+
return defaultValue;
|
|
391
|
+
}
|
|
392
|
+
current = current[segment];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (current === undefined) {
|
|
396
|
+
return defaultValue;
|
|
397
|
+
}
|
|
398
|
+
return current as T;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function pickString(source: JsonObject | null, keys: string[]): string {
|
|
402
|
+
if (!source) {
|
|
403
|
+
return "";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const key of keys) {
|
|
407
|
+
const value = source[key];
|
|
408
|
+
if (typeof value === "string" && value.length > 0) {
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return "";
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function extractWorldRawFromParsed(parsed: JsonObject): JsonObject {
|
|
417
|
+
const directKeys = ["character_book", "worldbook", "world_info", "lorebook"];
|
|
418
|
+
for (const key of directKeys) {
|
|
419
|
+
const candidate = asObject(parsed[key]);
|
|
420
|
+
if (candidate) {
|
|
421
|
+
return candidate;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const data = asObject(parsed.data);
|
|
426
|
+
if (data) {
|
|
427
|
+
for (const key of directKeys) {
|
|
428
|
+
const candidate = asObject(data[key]);
|
|
429
|
+
if (candidate) {
|
|
430
|
+
return candidate;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const extensions = data ? asObject(data.extensions) : asObject(parsed.extensions);
|
|
436
|
+
if (extensions) {
|
|
437
|
+
const extensionKeys = ["world", "worldbook", "character_book", "lorebook"];
|
|
438
|
+
for (const key of extensionKeys) {
|
|
439
|
+
const candidate = asObject(extensions[key]);
|
|
440
|
+
if (candidate) {
|
|
441
|
+
return candidate;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return parsed;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function normalizeWorldEntry(entry: JsonObject): WorldEntryInfo {
|
|
450
|
+
return {
|
|
451
|
+
raw: entry,
|
|
452
|
+
uid: typeof entry.uid === "number" || typeof entry.uid === "string" ? entry.uid : undefined,
|
|
453
|
+
keys: asStringArray(entry.keys),
|
|
454
|
+
secondaryKeys: asStringArray(entry.secondary_keys),
|
|
455
|
+
comment: asString(entry.comment),
|
|
456
|
+
content: asString(entry.content),
|
|
457
|
+
enabled: entry.enabled !== false,
|
|
458
|
+
order: asNumber(entry.order),
|
|
459
|
+
position:
|
|
460
|
+
typeof entry.position === "number" || typeof entry.position === "string"
|
|
461
|
+
? entry.position
|
|
462
|
+
: undefined,
|
|
463
|
+
probability: asNumber(entry.probability),
|
|
464
|
+
depth: asNumber(entry.depth),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function createWorldInfo(raw: JsonObject): WorldInfo {
|
|
469
|
+
const entriesRaw = Array.isArray(raw.entries) ? raw.entries : [];
|
|
470
|
+
const entries = entriesRaw
|
|
471
|
+
.map((entry) => asObject(entry))
|
|
472
|
+
.filter((entry): entry is JsonObject => entry !== null)
|
|
473
|
+
.map((entry) => normalizeWorldEntry(entry));
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
raw,
|
|
477
|
+
name: pickString(raw, ["name", "title", "world_name"]),
|
|
478
|
+
entries,
|
|
479
|
+
entryCount: entries.length,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function createCharacterInfo(raw: JsonObject): CharacterInfo {
|
|
484
|
+
const data = asObject(raw.data);
|
|
485
|
+
const worldRaw = extractWorldRawFromParsed(raw);
|
|
486
|
+
const worldEntries = Array.isArray(worldRaw.entries) ? worldRaw.entries : [];
|
|
487
|
+
const worldInfo = worldEntries.length > 0 ? createWorldInfo(worldRaw) : null;
|
|
488
|
+
const tagsFromData = asStringArray(data?.tags);
|
|
489
|
+
const tags = tagsFromData.length > 0 ? tagsFromData : asStringArray(raw.tags);
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
raw,
|
|
493
|
+
data,
|
|
494
|
+
name: pickString(data, ["name"]) || pickString(raw, ["name"]),
|
|
495
|
+
description: pickString(data, ["description"]) || pickString(raw, ["description"]),
|
|
496
|
+
personality: pickString(data, ["personality"]) || pickString(raw, ["personality"]),
|
|
497
|
+
scenario: pickString(data, ["scenario"]) || pickString(raw, ["scenario"]),
|
|
498
|
+
firstMessage: pickString(data, ["first_mes", "firstMessage"]) || pickString(raw, ["first_mes", "firstMessage"]),
|
|
499
|
+
exampleMessages: pickString(data, ["mes_example", "example_messages"]) || pickString(raw, ["mes_example", "example_messages"]),
|
|
500
|
+
tags,
|
|
501
|
+
creatorComment:
|
|
502
|
+
pickString(data, ["creator_notes", "creatorcomment"]) || pickString(raw, ["creator_notes", "creatorcomment"]),
|
|
503
|
+
avatar: pickString(data, ["avatar"]) || pickString(raw, ["avatar"]),
|
|
504
|
+
spec: pickString(raw, ["spec"]),
|
|
505
|
+
specVersion: pickString(raw, ["spec_version"]),
|
|
506
|
+
worldInfo,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function resolvePresetModel(raw: JsonObject, source: string): string {
|
|
511
|
+
const bySource: Record<string, string> = {
|
|
512
|
+
openai: "openai_model",
|
|
513
|
+
claude: "claude_model",
|
|
514
|
+
windowai: "windowai_model",
|
|
515
|
+
openrouter: "openrouter_model",
|
|
516
|
+
ai21: "ai21_model",
|
|
517
|
+
mistralai: "mistralai_model",
|
|
518
|
+
cohere: "cohere_model",
|
|
519
|
+
perplexity: "perplexity_model",
|
|
520
|
+
groq: "groq_model",
|
|
521
|
+
zerooneai: "zerooneai_model",
|
|
522
|
+
blockentropy: "blockentropy_model",
|
|
523
|
+
custom: "custom_model",
|
|
524
|
+
google: "google_model",
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const modelKey = bySource[source];
|
|
528
|
+
if (modelKey && typeof raw[modelKey] === "string") {
|
|
529
|
+
return asString(raw[modelKey]);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const fallbackKeys = Object.values(bySource);
|
|
533
|
+
for (const key of fallbackKeys) {
|
|
534
|
+
if (typeof raw[key] === "string" && asString(raw[key]).length > 0) {
|
|
535
|
+
return asString(raw[key]);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return "";
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function createPresetsInfo(raw: JsonObject): PresetsInfo {
|
|
543
|
+
const source = pickString(raw, ["chat_completion_source"]);
|
|
544
|
+
return {
|
|
545
|
+
raw,
|
|
546
|
+
source,
|
|
547
|
+
model: resolvePresetModel(raw, source),
|
|
548
|
+
temperature: asNumber(raw.temperature),
|
|
549
|
+
topP: asNumber(raw.top_p),
|
|
550
|
+
topK: asNumber(raw.top_k),
|
|
551
|
+
minP: asNumber(raw.min_p),
|
|
552
|
+
maxContext: asNumber(raw.openai_max_context),
|
|
553
|
+
maxTokens: asNumber(raw.openai_max_tokens),
|
|
554
|
+
seed: asNumber(raw.seed),
|
|
555
|
+
stream: asBoolean(raw.stream_openai),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export async function getCharacterInfo(input: FileInput): Promise<CharacterInfo> {
|
|
560
|
+
const raw = await parseAnyJson(input);
|
|
561
|
+
return createCharacterInfo(raw);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export async function getWorldInfo(input: FileInput): Promise<WorldInfo> {
|
|
565
|
+
const parsed = await parseAnyJson(input);
|
|
566
|
+
const worldRaw = extractWorldRawFromParsed(parsed);
|
|
567
|
+
return createWorldInfo(worldRaw);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export async function getPresetsInfo(input: FileInput): Promise<PresetsInfo> {
|
|
571
|
+
const raw = await parseAnyJson(input);
|
|
572
|
+
return createPresetsInfo(raw);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export function isCharacterInfo(value: unknown): value is CharacterInfo {
|
|
576
|
+
if (!isJsonObjectValue(value)) {
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return (
|
|
581
|
+
isJsonObjectValue(value.raw) &&
|
|
582
|
+
typeof value.name === "string" &&
|
|
583
|
+
Array.isArray(value.tags) &&
|
|
584
|
+
"worldInfo" in value
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function isWorldInfo(value: unknown): value is WorldInfo {
|
|
589
|
+
if (!isJsonObjectValue(value)) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return isJsonObjectValue(value.raw) && typeof value.name === "string" && Array.isArray(value.entries);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export function isPresetsInfo(value: unknown): value is PresetsInfo {
|
|
597
|
+
if (!isJsonObjectValue(value)) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return isJsonObjectValue(value.raw) && typeof value.source === "string" && typeof value.model === "string";
|
|
602
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"moduleResolution": "Node",
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|
|
15
|
+
|