koishi-plugin-memesluna 0.2.2 → 0.2.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/dist/index.js +1 -0
- package/lib/index.js +561 -54
- package/package.json +1 -1
- package/lib/config.js +0 -38
- package/lib/service.js +0 -518
package/lib/index.js
CHANGED
|
@@ -5,8 +5,8 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
5
5
|
var __getProtoOf = Object.getPrototypeOf;
|
|
6
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
7
|
var __export = (target, all) => {
|
|
8
|
-
for (var
|
|
9
|
-
__defProp(target,
|
|
8
|
+
for (var name2 in all)
|
|
9
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
var __copyProps = (to, from, except, desc) => {
|
|
12
12
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
@@ -16,7 +16,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
16
|
}
|
|
17
17
|
return to;
|
|
18
18
|
};
|
|
19
|
-
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
20
19
|
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
20
|
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
21
|
// file that has been converted to a CommonJS file using a Babel-
|
|
@@ -26,18 +25,518 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
25
|
mod
|
|
27
26
|
));
|
|
28
27
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/index.ts
|
|
29
30
|
var index_exports = {};
|
|
30
31
|
__export(index_exports, {
|
|
32
|
+
Config: () => Config,
|
|
33
|
+
MemesLunaService: () => MemesLunaService,
|
|
31
34
|
apply: () => apply,
|
|
32
|
-
inject: () => inject
|
|
35
|
+
inject: () => inject,
|
|
36
|
+
name: () => name
|
|
33
37
|
});
|
|
34
38
|
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
var import_promises2 = __toESM(require("fs/promises"));
|
|
40
|
+
var import_path2 = __toESM(require("path"));
|
|
41
|
+
|
|
42
|
+
// src/service.ts
|
|
43
|
+
var import_crypto = require("crypto");
|
|
35
44
|
var import_promises = __toESM(require("fs/promises"));
|
|
36
45
|
var import_path = __toESM(require("path"));
|
|
37
|
-
var
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
var import_koishi = require("koishi");
|
|
47
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
48
|
+
".jpg",
|
|
49
|
+
".jpeg",
|
|
50
|
+
".png",
|
|
51
|
+
".gif",
|
|
52
|
+
".bmp",
|
|
53
|
+
".webp",
|
|
54
|
+
".svg",
|
|
55
|
+
".tif",
|
|
56
|
+
".tiff",
|
|
57
|
+
".avif",
|
|
58
|
+
".psd"
|
|
59
|
+
]);
|
|
60
|
+
var COLLECTION_NAME_REGEXP = /^[a-zA-Z0-9_-]+$/;
|
|
61
|
+
var ENDPOINT_NAME_REGEXP = /^[a-zA-Z0-9_-]+$/;
|
|
62
|
+
var MemesLunaService = class extends import_koishi.Service {
|
|
63
|
+
constructor(ctx, config) {
|
|
64
|
+
super(ctx, "memesluna", true);
|
|
65
|
+
this.config = config;
|
|
66
|
+
this.defineDatabase();
|
|
67
|
+
this._readyPromise = new Promise((resolve) => {
|
|
68
|
+
this._readyResolve = resolve;
|
|
69
|
+
});
|
|
70
|
+
ctx.on("ready", async () => {
|
|
71
|
+
await this.ensureStorage();
|
|
72
|
+
this._readyResolve();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
_readyPromise;
|
|
76
|
+
_readyResolve;
|
|
77
|
+
static inject = ["database"];
|
|
78
|
+
get ready() {
|
|
79
|
+
return this._readyPromise;
|
|
80
|
+
}
|
|
81
|
+
defineDatabase() {
|
|
82
|
+
this.ctx.database.extend(
|
|
83
|
+
"memesluna_endpoints",
|
|
84
|
+
{
|
|
85
|
+
id: "string",
|
|
86
|
+
name: "string",
|
|
87
|
+
group: "string",
|
|
88
|
+
description: "string",
|
|
89
|
+
url: "string",
|
|
90
|
+
method: "string",
|
|
91
|
+
url_construction: "string",
|
|
92
|
+
model_name: "string",
|
|
93
|
+
query_params: "string",
|
|
94
|
+
proxy_settings: "string",
|
|
95
|
+
created_at: "timestamp",
|
|
96
|
+
updated_at: "timestamp"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
primary: "id",
|
|
100
|
+
unique: ["name"]
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
getStorageRoot() {
|
|
105
|
+
return import_path.default.resolve(this.ctx.baseDir, this.config.storagePath);
|
|
106
|
+
}
|
|
107
|
+
async ensureStorage() {
|
|
108
|
+
await import_promises.default.mkdir(this.getStorageRoot(), { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
ensureCollectionName(name2) {
|
|
111
|
+
if (!name2 || !COLLECTION_NAME_REGEXP.test(name2)) {
|
|
112
|
+
throw new Error("Invalid collection name: only letters, numbers, _ and - are allowed.");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
ensureEndpointName(name2) {
|
|
116
|
+
if (!name2 || !ENDPOINT_NAME_REGEXP.test(name2)) {
|
|
117
|
+
throw new Error("Invalid endpoint name: only letters, numbers, _ and - are allowed.");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
getCollectionDir(collectionName) {
|
|
121
|
+
return import_path.default.join(this.getStorageRoot(), collectionName);
|
|
122
|
+
}
|
|
123
|
+
getCollectionLinksFile(collectionName) {
|
|
124
|
+
return import_path.default.join(this.getCollectionDir(collectionName), `${collectionName}.txt`);
|
|
125
|
+
}
|
|
126
|
+
getCollectionDescriptionFile(collectionName) {
|
|
127
|
+
return import_path.default.join(this.getCollectionDir(collectionName), ".description");
|
|
128
|
+
}
|
|
129
|
+
async getCollectionDescription(collectionName) {
|
|
130
|
+
try {
|
|
131
|
+
return (await import_promises.default.readFile(this.getCollectionDescriptionFile(collectionName), "utf8")).trim();
|
|
132
|
+
} catch {
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async setCollectionDescription(collectionName, description) {
|
|
137
|
+
if (!await this.collectionExists(collectionName)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
await import_promises.default.writeFile(this.getCollectionDescriptionFile(collectionName), description.trim(), "utf8");
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
async collectionExists(collectionName) {
|
|
144
|
+
const dir = this.getCollectionDir(collectionName);
|
|
145
|
+
try {
|
|
146
|
+
const stat = await import_promises.default.stat(dir);
|
|
147
|
+
return stat.isDirectory();
|
|
148
|
+
} catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async getCollections() {
|
|
153
|
+
const root = this.getStorageRoot();
|
|
154
|
+
try {
|
|
155
|
+
const entries = await import_promises.default.readdir(root, { withFileTypes: true });
|
|
156
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
157
|
+
} catch {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async createCollection(collectionName) {
|
|
162
|
+
this.ensureCollectionName(collectionName);
|
|
163
|
+
const dir = this.getCollectionDir(collectionName);
|
|
164
|
+
try {
|
|
165
|
+
await import_promises.default.mkdir(dir);
|
|
166
|
+
return true;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error.code === "EEXIST") {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async deleteCollection(collectionName) {
|
|
175
|
+
this.ensureCollectionName(collectionName);
|
|
176
|
+
const dir = this.getCollectionDir(collectionName);
|
|
177
|
+
try {
|
|
178
|
+
await import_promises.default.rm(dir, { recursive: true, force: true });
|
|
179
|
+
return true;
|
|
180
|
+
} catch {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
isImageFile(filename) {
|
|
185
|
+
const ext = import_path.default.extname(filename).toLowerCase();
|
|
186
|
+
return IMAGE_EXTENSIONS.has(ext);
|
|
187
|
+
}
|
|
188
|
+
ensureSafeImageFilename(filename) {
|
|
189
|
+
const normalized = import_path.default.basename(filename || "");
|
|
190
|
+
if (!filename || normalized !== filename || filename.includes("/") || filename.includes("\\")) {
|
|
191
|
+
throw new Error("Invalid image filename");
|
|
192
|
+
}
|
|
193
|
+
if (!this.isImageFile(normalized)) {
|
|
194
|
+
throw new Error("Invalid image filename");
|
|
195
|
+
}
|
|
196
|
+
return normalized;
|
|
197
|
+
}
|
|
198
|
+
getMimeByFilename(filename) {
|
|
199
|
+
const ext = import_path.default.extname(filename).toLowerCase();
|
|
200
|
+
if (ext === ".png") return "image/png";
|
|
201
|
+
if (ext === ".gif") return "image/gif";
|
|
202
|
+
if (ext === ".webp") return "image/webp";
|
|
203
|
+
if (ext === ".bmp") return "image/bmp";
|
|
204
|
+
if (ext === ".svg") return "image/svg+xml";
|
|
205
|
+
if (ext === ".avif") return "image/avif";
|
|
206
|
+
if (ext === ".tif" || ext === ".tiff") return "image/tiff";
|
|
207
|
+
return "image/jpeg";
|
|
208
|
+
}
|
|
209
|
+
resolveLocalImagePath(collectionName, filename) {
|
|
210
|
+
const safeName = this.ensureSafeImageFilename(filename);
|
|
211
|
+
return import_path.default.join(this.getCollectionDir(collectionName), safeName);
|
|
212
|
+
}
|
|
213
|
+
async getLocalImageBuffer(collectionName, filename) {
|
|
214
|
+
if (!await this.collectionExists(collectionName)) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
let fullPath;
|
|
218
|
+
try {
|
|
219
|
+
fullPath = this.resolveLocalImagePath(collectionName, filename);
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const buffer = await import_promises.default.readFile(fullPath);
|
|
225
|
+
return {
|
|
226
|
+
buffer,
|
|
227
|
+
mime: this.getMimeByFilename(fullPath)
|
|
228
|
+
};
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async getCollectionImages(collectionName) {
|
|
234
|
+
if (!await this.collectionExists(collectionName)) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
const dir = this.getCollectionDir(collectionName);
|
|
238
|
+
const entries = await import_promises.default.readdir(dir, { withFileTypes: true });
|
|
239
|
+
return entries.filter((entry) => entry.isFile() && this.isImageFile(entry.name)).map((entry) => entry.name).sort();
|
|
240
|
+
}
|
|
241
|
+
async getCollectionLinks(collectionName) {
|
|
242
|
+
if (!await this.collectionExists(collectionName)) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
const linksPath = this.getCollectionLinksFile(collectionName);
|
|
246
|
+
try {
|
|
247
|
+
const text = await import_promises.default.readFile(linksPath, "utf8");
|
|
248
|
+
return text.split(/\r?\n/g).map((line) => line.trim()).filter((line) => line.startsWith("http://") || line.startsWith("https://"));
|
|
249
|
+
} catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async addLinksToCollection(collectionName, links) {
|
|
254
|
+
if (!await this.collectionExists(collectionName)) {
|
|
255
|
+
throw new Error(`Collection not found: ${collectionName}`);
|
|
256
|
+
}
|
|
257
|
+
const normalized = links.map((link) => link.trim()).filter((link) => link.startsWith("http://") || link.startsWith("https://"));
|
|
258
|
+
if (!normalized.length) {
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
|
261
|
+
const current = await this.getCollectionLinks(collectionName);
|
|
262
|
+
const merged = [...current];
|
|
263
|
+
for (const link of normalized) {
|
|
264
|
+
if (!merged.includes(link)) {
|
|
265
|
+
merged.push(link);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const linksPath = this.getCollectionLinksFile(collectionName);
|
|
269
|
+
await import_promises.default.writeFile(linksPath, `${merged.join("\n")}${merged.length ? "\n" : ""}`, "utf8");
|
|
270
|
+
return merged.length - current.length;
|
|
271
|
+
}
|
|
272
|
+
async removeLinkFromCollection(collectionName, link) {
|
|
273
|
+
if (!await this.collectionExists(collectionName)) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
const current = await this.getCollectionLinks(collectionName);
|
|
277
|
+
const next = current.filter((item) => item !== link);
|
|
278
|
+
if (next.length === current.length) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
const linksPath = this.getCollectionLinksFile(collectionName);
|
|
282
|
+
await import_promises.default.writeFile(linksPath, `${next.join("\n")}${next.length ? "\n" : ""}`, "utf8");
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
detectExtFromDataUrl(dataUrl) {
|
|
286
|
+
const matched = /^data:image\/([a-zA-Z0-9+.-]+);base64,/i.exec(dataUrl);
|
|
287
|
+
const ext = matched?.[1]?.toLowerCase();
|
|
288
|
+
if (!ext) return "png";
|
|
289
|
+
if (ext === "jpeg") return "jpg";
|
|
290
|
+
return ext;
|
|
291
|
+
}
|
|
292
|
+
normalizeBase64(input) {
|
|
293
|
+
const trimmed = input.trim();
|
|
294
|
+
if (trimmed.startsWith("data:")) {
|
|
295
|
+
const extHint = this.detectExtFromDataUrl(trimmed);
|
|
296
|
+
const index = trimmed.indexOf(",");
|
|
297
|
+
return {
|
|
298
|
+
base64: index >= 0 ? trimmed.slice(index + 1) : trimmed,
|
|
299
|
+
extHint
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return { base64: trimmed };
|
|
303
|
+
}
|
|
304
|
+
buildSafeFilename(originalName, extHint) {
|
|
305
|
+
const fallbackName = `${Date.now()}-${Math.floor(Math.random() * 1e4)}`;
|
|
306
|
+
const src = (originalName ?? fallbackName).trim();
|
|
307
|
+
const parsed = import_path.default.parse(src);
|
|
308
|
+
const sanitizedBase = (parsed.name || fallbackName).replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64);
|
|
309
|
+
const rawExt = (parsed.ext || (extHint ? `.${extHint}` : "") || ".png").toLowerCase();
|
|
310
|
+
const normalizedExt = rawExt === ".jpeg" ? ".jpg" : rawExt;
|
|
311
|
+
const finalExt = IMAGE_EXTENSIONS.has(normalizedExt) ? normalizedExt : ".png";
|
|
312
|
+
return `${sanitizedBase}${finalExt}`;
|
|
313
|
+
}
|
|
314
|
+
async deduplicateFilename(collectionDir, filename) {
|
|
315
|
+
const parsed = import_path.default.parse(filename);
|
|
316
|
+
let counter = 1;
|
|
317
|
+
let candidate = filename;
|
|
318
|
+
while (true) {
|
|
319
|
+
try {
|
|
320
|
+
await import_promises.default.access(import_path.default.join(collectionDir, candidate));
|
|
321
|
+
candidate = `${parsed.name}_${counter}${parsed.ext}`;
|
|
322
|
+
counter++;
|
|
323
|
+
} catch {
|
|
324
|
+
return candidate;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async addLocalImageBase64(collectionName, base64Data, originalName) {
|
|
329
|
+
if (!await this.collectionExists(collectionName)) {
|
|
330
|
+
throw new Error(`Collection not found: ${collectionName}`);
|
|
331
|
+
}
|
|
332
|
+
const { base64, extHint } = this.normalizeBase64(base64Data);
|
|
333
|
+
const buffer = Buffer.from(base64, "base64");
|
|
334
|
+
if (!buffer.length) {
|
|
335
|
+
throw new Error("Invalid image base64 payload");
|
|
336
|
+
}
|
|
337
|
+
const dir = this.getCollectionDir(collectionName);
|
|
338
|
+
const initialName = this.buildSafeFilename(originalName, extHint);
|
|
339
|
+
const finalName = await this.deduplicateFilename(dir, initialName);
|
|
340
|
+
await import_promises.default.writeFile(import_path.default.join(dir, finalName), buffer);
|
|
341
|
+
return finalName;
|
|
342
|
+
}
|
|
343
|
+
async deleteImageFromCollection(collectionName, filename) {
|
|
344
|
+
if (!await this.collectionExists(collectionName)) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
let safeName;
|
|
348
|
+
try {
|
|
349
|
+
safeName = this.ensureSafeImageFilename(filename);
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const fullPath = import_path.default.join(this.getCollectionDir(collectionName), safeName);
|
|
354
|
+
try {
|
|
355
|
+
await import_promises.default.unlink(fullPath);
|
|
356
|
+
return true;
|
|
357
|
+
} catch {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async moveImageToCollection(sourceCollection, targetCollection, filename) {
|
|
362
|
+
if (!await this.collectionExists(sourceCollection) || !await this.collectionExists(targetCollection)) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
let safeName;
|
|
366
|
+
try {
|
|
367
|
+
safeName = this.ensureSafeImageFilename(filename);
|
|
368
|
+
} catch {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
const sourcePath = import_path.default.join(this.getCollectionDir(sourceCollection), safeName);
|
|
372
|
+
const targetDir = this.getCollectionDir(targetCollection);
|
|
373
|
+
const targetName = await this.deduplicateFilename(targetDir, safeName);
|
|
374
|
+
const targetPath = import_path.default.join(targetDir, targetName);
|
|
375
|
+
try {
|
|
376
|
+
await import_promises.default.rename(sourcePath, targetPath);
|
|
377
|
+
return targetName;
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async getCollectionInfo(collectionName) {
|
|
383
|
+
if (!await this.collectionExists(collectionName)) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const localImages = await this.getCollectionImages(collectionName);
|
|
387
|
+
const links = await this.getCollectionLinks(collectionName);
|
|
388
|
+
const description = await this.getCollectionDescription(collectionName);
|
|
389
|
+
return {
|
|
390
|
+
name: collectionName,
|
|
391
|
+
description,
|
|
392
|
+
localCount: localImages.length,
|
|
393
|
+
linkCount: links.length,
|
|
394
|
+
totalCount: localImages.length + links.length,
|
|
395
|
+
hasContent: localImages.length > 0 || links.length > 0,
|
|
396
|
+
cover: localImages[0]
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
async getRandomResource(collectionName) {
|
|
400
|
+
if (!await this.collectionExists(collectionName)) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
const localImages = await this.getCollectionImages(collectionName);
|
|
404
|
+
const links = await this.getCollectionLinks(collectionName);
|
|
405
|
+
const pool = [
|
|
406
|
+
...localImages.map((name2) => ({
|
|
407
|
+
type: "local",
|
|
408
|
+
value: import_path.default.join(this.getCollectionDir(collectionName), name2)
|
|
409
|
+
})),
|
|
410
|
+
...links.map((link) => ({ type: "external", value: link }))
|
|
411
|
+
];
|
|
412
|
+
if (!pool.length) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
return pool[Math.floor(Math.random() * pool.length)];
|
|
416
|
+
}
|
|
417
|
+
parseJsonField(value, fallback) {
|
|
418
|
+
if (!value) return fallback;
|
|
419
|
+
try {
|
|
420
|
+
return JSON.parse(value);
|
|
421
|
+
} catch {
|
|
422
|
+
return fallback;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
mapEndpoint(row) {
|
|
426
|
+
return {
|
|
427
|
+
id: row.id,
|
|
428
|
+
name: row.name,
|
|
429
|
+
group: row.group || "\u9ED8\u8BA4\u5206\u7EC4",
|
|
430
|
+
description: row.description || "",
|
|
431
|
+
url: row.url,
|
|
432
|
+
method: row.method || "redirect",
|
|
433
|
+
urlConstruction: row.url_construction || "normal",
|
|
434
|
+
modelName: row.model_name || "",
|
|
435
|
+
queryParams: this.parseJsonField(row.query_params, []),
|
|
436
|
+
proxySettings: this.parseJsonField(row.proxy_settings, {
|
|
437
|
+
fallbackAction: "returnJson"
|
|
438
|
+
}),
|
|
439
|
+
createdAt: row.created_at,
|
|
440
|
+
updatedAt: row.updated_at
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
async getEndpoints() {
|
|
444
|
+
const rows = await this.ctx.database.get("memesluna_endpoints", {});
|
|
445
|
+
return rows.map((row) => this.mapEndpoint(row)).sort((a, b) => a.name.localeCompare(b.name));
|
|
446
|
+
}
|
|
447
|
+
async getEndpointByName(name2) {
|
|
448
|
+
const rows = await this.ctx.database.get("memesluna_endpoints", { name: name2 });
|
|
449
|
+
if (!rows.length) return null;
|
|
450
|
+
return this.mapEndpoint(rows[0]);
|
|
451
|
+
}
|
|
452
|
+
async addEndpoint(input) {
|
|
453
|
+
this.ensureEndpointName(input.name);
|
|
454
|
+
if (!input.url) {
|
|
455
|
+
throw new Error("Endpoint URL is required.");
|
|
456
|
+
}
|
|
457
|
+
const id = (0, import_crypto.randomUUID)();
|
|
458
|
+
const now = /* @__PURE__ */ new Date();
|
|
459
|
+
await this.ctx.database.create("memesluna_endpoints", {
|
|
460
|
+
id,
|
|
461
|
+
name: input.name,
|
|
462
|
+
group: input.group || "\u9ED8\u8BA4\u5206\u7EC4",
|
|
463
|
+
description: input.description || "",
|
|
464
|
+
url: input.url,
|
|
465
|
+
method: input.method || "redirect",
|
|
466
|
+
url_construction: input.urlConstruction || "normal",
|
|
467
|
+
model_name: input.modelName || "",
|
|
468
|
+
query_params: JSON.stringify(input.queryParams || []),
|
|
469
|
+
proxy_settings: JSON.stringify({
|
|
470
|
+
fallbackAction: "returnJson",
|
|
471
|
+
...input.proxySettings || {}
|
|
472
|
+
}),
|
|
473
|
+
created_at: now,
|
|
474
|
+
updated_at: now
|
|
475
|
+
});
|
|
476
|
+
return id;
|
|
477
|
+
}
|
|
478
|
+
async updateEndpoint(name2, input) {
|
|
479
|
+
const current = await this.getEndpointByName(name2);
|
|
480
|
+
if (!current) {
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
const payload = {
|
|
484
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
485
|
+
};
|
|
486
|
+
if (input.group !== void 0) payload.group = input.group || "\u9ED8\u8BA4\u5206\u7EC4";
|
|
487
|
+
if (input.description !== void 0) payload.description = input.description || "";
|
|
488
|
+
if (input.url !== void 0) payload.url = input.url;
|
|
489
|
+
if (input.method !== void 0) payload.method = input.method;
|
|
490
|
+
if (input.urlConstruction !== void 0) payload.url_construction = input.urlConstruction;
|
|
491
|
+
if (input.modelName !== void 0) payload.model_name = input.modelName;
|
|
492
|
+
if (input.queryParams !== void 0) payload.query_params = JSON.stringify(input.queryParams);
|
|
493
|
+
if (input.proxySettings !== void 0) payload.proxy_settings = JSON.stringify(input.proxySettings);
|
|
494
|
+
await this.ctx.database.set("memesluna_endpoints", { name: name2 }, payload);
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
async deleteEndpoint(name2) {
|
|
498
|
+
const before = await this.ctx.database.get("memesluna_endpoints", { name: name2 });
|
|
499
|
+
if (!before.length) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
await this.ctx.database.remove("memesluna_endpoints", { name: name2 });
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
async buildRouteInventory(backendPath) {
|
|
506
|
+
const endpoints = await this.getEndpoints();
|
|
507
|
+
const collections = await this.getCollections();
|
|
508
|
+
const lines = [];
|
|
509
|
+
for (const endpoint of endpoints) {
|
|
510
|
+
const queryPart = endpoint.queryParams.filter((param) => param.required).map((param) => `${param.name}=<${param.name}>`).join("&");
|
|
511
|
+
const suffix = queryPart ? `?${queryPart}` : "";
|
|
512
|
+
const desc = endpoint.description || endpoint.name;
|
|
513
|
+
lines.push(`- ${endpoint.name} ${desc} ${backendPath}/${endpoint.name}${suffix}`);
|
|
514
|
+
}
|
|
515
|
+
for (const collection of collections) {
|
|
516
|
+
const info = await this.getCollectionInfo(collection);
|
|
517
|
+
if (info?.hasContent) {
|
|
518
|
+
const desc = info.description || collection;
|
|
519
|
+
lines.push(`- ${collection} ${desc} ${backendPath}/${collection}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return lines.join("\n");
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// src/config.ts
|
|
527
|
+
var import_koishi2 = require("koishi");
|
|
528
|
+
var Config = import_koishi2.Schema.object({
|
|
529
|
+
backendPath: import_koishi2.Schema.string().default("/memesluna").description("\u540E\u7AEF\u670D\u52A1\u8DEF\u5F84\u524D\u7F00"),
|
|
530
|
+
storagePath: import_koishi2.Schema.string().default("data/memesluna").description("\u672C\u5730\u5408\u96C6\u5B58\u50A8\u76EE\u5F55"),
|
|
531
|
+
selfUrl: import_koishi2.Schema.string().default("").description("\u670D\u52A1\u516C\u5F00\u5730\u5740\uFF0C\u4E0D\u586B\u5219\u4F18\u5148\u4F7F\u7528 server.selfUrl"),
|
|
532
|
+
injectVariables: import_koishi2.Schema.boolean().default(true).description("\u662F\u5426\u5411 ChatLuna \u6CE8\u5165 {{endpoint}} \u548C {{memesluna}} \u53D8\u91CF"),
|
|
533
|
+
variableRefreshIntervalMs: import_koishi2.Schema.number().min(30 * 1e3).max(60 * 60 * 1e3).default(5 * 60 * 1e3).description("\u53D8\u91CF\u5237\u65B0\u95F4\u9694\uFF08\u6BEB\u79D2\uFF09"),
|
|
534
|
+
injectVariablesPrompt: import_koishi2.Schema.string().role("textarea").default(`\u4F60\u53EF\u4EE5\u4F7F\u7528\u8868\u60C5\u5305\u6765\u4E30\u5BCC\u4F60\u7684\u56DE\u590D\u3002\u8868\u60C5\u5305\u5217\u8868\u662F{endpoint}\uFF0C\u57FA\u7840URL\u662F{base_url}\uFF0C\u4F60\u8981\u628A\u57FA\u7840URL\u62FC\u63A5\u5230\u8DEF\u5F84\u524D\u9762,\u4E0D\u8981\u52A0\u6587\u4EF6\u540D,\u53EA\u52A0\u8DEF\u5F84,\u7528\u53D1\u9001\u56FE\u7247\u7684\u65B9\u5F0F\u53D1\u9001\u3002`).description("\u6CE8\u5165\u5230 ChatLuna {{memesluna}} \u53D8\u91CF\u7684\u63D0\u793A\u8BCD\u6A21\u677F\uFF0C\u652F\u6301 {endpoint} \u548C {base_url} \u5360\u4F4D\u7B26")
|
|
535
|
+
});
|
|
536
|
+
var name = "memesluna";
|
|
537
|
+
|
|
538
|
+
// src/index.ts
|
|
539
|
+
var RESERVED_PATHS = /* @__PURE__ */ new Set([
|
|
41
540
|
"config",
|
|
42
541
|
"admin",
|
|
43
542
|
"admin-login",
|
|
@@ -51,9 +550,9 @@ const RESERVED_PATHS = /* @__PURE__ */ new Set([
|
|
|
51
550
|
"static",
|
|
52
551
|
"favicon.ico"
|
|
53
552
|
]);
|
|
54
|
-
|
|
55
|
-
function isReservedPath(
|
|
56
|
-
return RESERVED_PATHS.has(
|
|
553
|
+
var IMAGE_URL_REGEXP = /\.(jpeg|jpg|gif|png|webp|bmp|svg)(\?.*)?$/i;
|
|
554
|
+
function isReservedPath(name2) {
|
|
555
|
+
return RESERVED_PATHS.has(name2) || name2.includes(".");
|
|
57
556
|
}
|
|
58
557
|
function getValueByDotNotation(obj, dotPath) {
|
|
59
558
|
if (!dotPath) return void 0;
|
|
@@ -73,7 +572,7 @@ function normalizeContentType(contentType) {
|
|
|
73
572
|
return contentType.toLowerCase().split(";")[0].trim();
|
|
74
573
|
}
|
|
75
574
|
function guessMimeByExt(filePath) {
|
|
76
|
-
const ext =
|
|
575
|
+
const ext = import_path2.default.extname(filePath).toLowerCase();
|
|
77
576
|
switch (ext) {
|
|
78
577
|
case ".png":
|
|
79
578
|
return "image/png";
|
|
@@ -229,23 +728,23 @@ async function applyDynamicForward(ctx, config, service, routeName, query) {
|
|
|
229
728
|
const validated = new URLSearchParams();
|
|
230
729
|
const errors = [];
|
|
231
730
|
for (const param of endpoint.queryParams) {
|
|
232
|
-
const
|
|
233
|
-
const raw = query[
|
|
731
|
+
const name2 = param.name;
|
|
732
|
+
const raw = query[name2];
|
|
234
733
|
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
235
734
|
if (typeof value === "string") {
|
|
236
735
|
if (param.validValues && param.validValues.length > 0 && !param.validValues.includes(value)) {
|
|
237
|
-
errors.push(`Invalid value for '${
|
|
736
|
+
errors.push(`Invalid value for '${name2}'`);
|
|
238
737
|
} else {
|
|
239
|
-
validated.set(
|
|
738
|
+
validated.set(name2, value);
|
|
240
739
|
}
|
|
241
740
|
continue;
|
|
242
741
|
}
|
|
243
742
|
if (param.required) {
|
|
244
|
-
errors.push(`Missing required parameter: ${
|
|
743
|
+
errors.push(`Missing required parameter: ${name2}`);
|
|
245
744
|
continue;
|
|
246
745
|
}
|
|
247
746
|
if (param.defaultValue !== void 0) {
|
|
248
|
-
validated.set(
|
|
747
|
+
validated.set(name2, param.defaultValue);
|
|
249
748
|
}
|
|
250
749
|
}
|
|
251
750
|
if (errors.length > 0) {
|
|
@@ -278,7 +777,7 @@ async function applyDynamicForward(ctx, config, service, routeName, query) {
|
|
|
278
777
|
if (resource.type === "external") {
|
|
279
778
|
return { redirectTo: resource.value };
|
|
280
779
|
}
|
|
281
|
-
const fileBuffer = await
|
|
780
|
+
const fileBuffer = await import_promises2.default.readFile(resource.value);
|
|
282
781
|
return {
|
|
283
782
|
status: 200,
|
|
284
783
|
body: fileBuffer,
|
|
@@ -352,7 +851,7 @@ function normalizeUrlConstruction(value) {
|
|
|
352
851
|
async function buildAdminState(service) {
|
|
353
852
|
const endpoints = await service.getEndpoints();
|
|
354
853
|
const collectionNames = await service.getCollections();
|
|
355
|
-
const collections = await Promise.all(collectionNames.map((
|
|
854
|
+
const collections = await Promise.all(collectionNames.map((name2) => service.getCollectionInfo(name2)));
|
|
356
855
|
return {
|
|
357
856
|
endpoints,
|
|
358
857
|
collectionNames,
|
|
@@ -588,6 +1087,9 @@ function buildHomepageHtml(basePath) {
|
|
|
588
1087
|
<li class="nav-item">
|
|
589
1088
|
<a class="nav-link" href="${basePath}/admin">\u7BA1\u7406</a>
|
|
590
1089
|
</li>
|
|
1090
|
+
<li class="nav-item">
|
|
1091
|
+
<a class="nav-link" href="${basePath}/admin/endpoint">\u7AEF\u70B9</a>
|
|
1092
|
+
</li>
|
|
591
1093
|
</ul>
|
|
592
1094
|
</div>
|
|
593
1095
|
</div>
|
|
@@ -1798,7 +2300,7 @@ async function updateMemesVariable(ctx, config, service) {
|
|
|
1798
2300
|
function applyConsole(ctx, config, service) {
|
|
1799
2301
|
if (!ctx.console) return;
|
|
1800
2302
|
const consoleService = ctx.console;
|
|
1801
|
-
const packageBase =
|
|
2303
|
+
const packageBase = import_path2.default.resolve(ctx.baseDir, "node_modules/koishi-plugin-memesluna");
|
|
1802
2304
|
const withReady = (handler) => {
|
|
1803
2305
|
return async (...args) => {
|
|
1804
2306
|
await service.ready;
|
|
@@ -1806,8 +2308,8 @@ function applyConsole(ctx, config, service) {
|
|
|
1806
2308
|
};
|
|
1807
2309
|
};
|
|
1808
2310
|
consoleService.addEntry({
|
|
1809
|
-
dev:
|
|
1810
|
-
prod:
|
|
2311
|
+
dev: import_path2.default.resolve(packageBase, "client/index.ts"),
|
|
2312
|
+
prod: import_path2.default.resolve(packageBase, "dist")
|
|
1811
2313
|
});
|
|
1812
2314
|
consoleService.addListener(
|
|
1813
2315
|
"memesluna/getState",
|
|
@@ -1815,7 +2317,7 @@ function applyConsole(ctx, config, service) {
|
|
|
1815
2317
|
const endpoints = await service.getEndpoints();
|
|
1816
2318
|
const collections = await service.getCollections();
|
|
1817
2319
|
const detailedCollections = await Promise.all(
|
|
1818
|
-
collections.map(async (
|
|
2320
|
+
collections.map(async (name2) => service.getCollectionInfo(name2))
|
|
1819
2321
|
);
|
|
1820
2322
|
return {
|
|
1821
2323
|
backendPath: config.backendPath,
|
|
@@ -1826,20 +2328,20 @@ function applyConsole(ctx, config, service) {
|
|
|
1826
2328
|
);
|
|
1827
2329
|
consoleService.addListener(
|
|
1828
2330
|
"memesluna/createCollection",
|
|
1829
|
-
withReady(async (
|
|
1830
|
-
return await service.createCollection(
|
|
2331
|
+
withReady(async (name2) => {
|
|
2332
|
+
return await service.createCollection(name2);
|
|
1831
2333
|
})
|
|
1832
2334
|
);
|
|
1833
2335
|
consoleService.addListener(
|
|
1834
2336
|
"memesluna/deleteCollection",
|
|
1835
|
-
withReady(async (
|
|
1836
|
-
return await service.deleteCollection(
|
|
2337
|
+
withReady(async (name2) => {
|
|
2338
|
+
return await service.deleteCollection(name2);
|
|
1837
2339
|
})
|
|
1838
2340
|
);
|
|
1839
2341
|
consoleService.addListener(
|
|
1840
2342
|
"memesluna/setCollectionDescription",
|
|
1841
|
-
withReady(async (
|
|
1842
|
-
return await service.setCollectionDescription(
|
|
2343
|
+
withReady(async (name2, description) => {
|
|
2344
|
+
return await service.setCollectionDescription(name2, description);
|
|
1843
2345
|
})
|
|
1844
2346
|
);
|
|
1845
2347
|
consoleService.addListener(
|
|
@@ -1881,14 +2383,14 @@ function applyConsole(ctx, config, service) {
|
|
|
1881
2383
|
);
|
|
1882
2384
|
consoleService.addListener(
|
|
1883
2385
|
"memesluna/updateEndpoint",
|
|
1884
|
-
withReady(async (
|
|
1885
|
-
return await service.updateEndpoint(
|
|
2386
|
+
withReady(async (name2, payload) => {
|
|
2387
|
+
return await service.updateEndpoint(name2, payload);
|
|
1886
2388
|
})
|
|
1887
2389
|
);
|
|
1888
2390
|
consoleService.addListener(
|
|
1889
2391
|
"memesluna/deleteEndpoint",
|
|
1890
|
-
withReady(async (
|
|
1891
|
-
return await service.deleteEndpoint(
|
|
2392
|
+
withReady(async (name2) => {
|
|
2393
|
+
return await service.deleteEndpoint(name2);
|
|
1892
2394
|
})
|
|
1893
2395
|
);
|
|
1894
2396
|
consoleService.addListener("memesluna/getBaseUrl", async () => {
|
|
@@ -1902,7 +2404,7 @@ function applyServer(ctx, config, service) {
|
|
|
1902
2404
|
const baseUrl = toAbsoluteBaseUrl(ctx, config);
|
|
1903
2405
|
const endpoints = await service.getEndpoints();
|
|
1904
2406
|
const collections = await service.getCollections();
|
|
1905
|
-
const collectionInfos = await Promise.all(collections.map((
|
|
2407
|
+
const collectionInfos = await Promise.all(collections.map((name2) => service.getCollectionInfo(name2)));
|
|
1906
2408
|
const inventory = await service.buildRouteInventory(basePath);
|
|
1907
2409
|
const llmPrompt = config.injectVariablesPrompt.replace("{endpoint}", inventory || "- \u6682\u65E0\u53EF\u7528\u8DEF\u7531").replace("{base_url}", baseUrl);
|
|
1908
2410
|
koa.body = {
|
|
@@ -1917,14 +2419,14 @@ function applyServer(ctx, config, service) {
|
|
|
1917
2419
|
});
|
|
1918
2420
|
ctx.server.post(`${basePath}/api/admin/collections`, async (koa) => {
|
|
1919
2421
|
const body = getRequestBody(koa);
|
|
1920
|
-
const
|
|
1921
|
-
if (!
|
|
2422
|
+
const name2 = toTrimmedString(body.name);
|
|
2423
|
+
if (!name2) {
|
|
1922
2424
|
koa.status = 400;
|
|
1923
2425
|
koa.body = { error: "Collection name is required" };
|
|
1924
2426
|
return;
|
|
1925
2427
|
}
|
|
1926
2428
|
try {
|
|
1927
|
-
const created = await service.createCollection(
|
|
2429
|
+
const created = await service.createCollection(name2);
|
|
1928
2430
|
if (!created) {
|
|
1929
2431
|
koa.status = 409;
|
|
1930
2432
|
koa.body = { error: "Collection already exists" };
|
|
@@ -1937,13 +2439,13 @@ function applyServer(ctx, config, service) {
|
|
|
1937
2439
|
}
|
|
1938
2440
|
});
|
|
1939
2441
|
ctx.server.delete(`${basePath}/api/admin/collections/:name`, async (koa) => {
|
|
1940
|
-
const
|
|
1941
|
-
if (!
|
|
2442
|
+
const name2 = toTrimmedString(koa.params.name);
|
|
2443
|
+
if (!name2) {
|
|
1942
2444
|
koa.status = 400;
|
|
1943
2445
|
koa.body = { error: "Collection name is required" };
|
|
1944
2446
|
return;
|
|
1945
2447
|
}
|
|
1946
|
-
const deleted = await service.deleteCollection(
|
|
2448
|
+
const deleted = await service.deleteCollection(name2);
|
|
1947
2449
|
if (!deleted) {
|
|
1948
2450
|
koa.status = 404;
|
|
1949
2451
|
koa.body = { error: "Collection not found" };
|
|
@@ -1952,10 +2454,10 @@ function applyServer(ctx, config, service) {
|
|
|
1952
2454
|
koa.body = { ok: true };
|
|
1953
2455
|
});
|
|
1954
2456
|
ctx.server.patch(`${basePath}/api/admin/collections/:name/description`, async (koa) => {
|
|
1955
|
-
const
|
|
2457
|
+
const name2 = toTrimmedString(koa.params.name);
|
|
1956
2458
|
const body = getRequestBody(koa);
|
|
1957
2459
|
const description = toTrimmedString(body.description);
|
|
1958
|
-
const updated = await service.setCollectionDescription(
|
|
2460
|
+
const updated = await service.setCollectionDescription(name2, description);
|
|
1959
2461
|
if (!updated) {
|
|
1960
2462
|
koa.status = 404;
|
|
1961
2463
|
koa.body = { error: "Collection not found" };
|
|
@@ -2066,15 +2568,15 @@ function applyServer(ctx, config, service) {
|
|
|
2066
2568
|
});
|
|
2067
2569
|
ctx.server.post(`${basePath}/api/admin/endpoints`, async (koa) => {
|
|
2068
2570
|
const body = getRequestBody(koa);
|
|
2069
|
-
const
|
|
2571
|
+
const name2 = toTrimmedString(body.name);
|
|
2070
2572
|
const url = toTrimmedString(body.url);
|
|
2071
|
-
if (!
|
|
2573
|
+
if (!name2 || !url) {
|
|
2072
2574
|
koa.status = 400;
|
|
2073
2575
|
koa.body = { error: "name and url are required" };
|
|
2074
2576
|
return;
|
|
2075
2577
|
}
|
|
2076
2578
|
const payload = {
|
|
2077
|
-
name,
|
|
2579
|
+
name: name2,
|
|
2078
2580
|
group: toTrimmedString(body.group) || "\u9ED8\u8BA4\u5206\u7EC4",
|
|
2079
2581
|
description: toTrimmedString(body.description),
|
|
2080
2582
|
url,
|
|
@@ -2116,8 +2618,8 @@ function applyServer(ctx, config, service) {
|
|
|
2116
2618
|
koa.body = { ok: true };
|
|
2117
2619
|
});
|
|
2118
2620
|
ctx.server.delete(`${basePath}/api/admin/endpoints/:name`, async (koa) => {
|
|
2119
|
-
const
|
|
2120
|
-
const deleted = await service.deleteEndpoint(
|
|
2621
|
+
const name2 = toTrimmedString(koa.params.name);
|
|
2622
|
+
const deleted = await service.deleteEndpoint(name2);
|
|
2121
2623
|
if (!deleted) {
|
|
2122
2624
|
koa.status = 404;
|
|
2123
2625
|
koa.body = { error: "Endpoint not found" };
|
|
@@ -2126,10 +2628,14 @@ function applyServer(ctx, config, service) {
|
|
|
2126
2628
|
koa.body = { ok: true };
|
|
2127
2629
|
});
|
|
2128
2630
|
ctx.server.get(`${basePath}/admin`, async (koa) => {
|
|
2129
|
-
koa.
|
|
2631
|
+
koa.status = 200;
|
|
2632
|
+
koa.set("Content-Type", "text/html; charset=utf-8");
|
|
2633
|
+
koa.body = buildAdminHtml(basePath);
|
|
2130
2634
|
});
|
|
2131
2635
|
ctx.server.get(`${basePath}/admin/endpoint`, async (koa) => {
|
|
2132
|
-
koa.
|
|
2636
|
+
koa.status = 200;
|
|
2637
|
+
koa.set("Content-Type", "text/html; charset=utf-8");
|
|
2638
|
+
koa.body = buildAdminEndpointHtml(basePath);
|
|
2133
2639
|
});
|
|
2134
2640
|
ctx.server.get(`${basePath}/api/collections/:name/resources`, async (koa) => {
|
|
2135
2641
|
const collectionName = koa.params.name;
|
|
@@ -2164,7 +2670,7 @@ function applyServer(ctx, config, service) {
|
|
|
2164
2670
|
});
|
|
2165
2671
|
}
|
|
2166
2672
|
function apply(ctx, config) {
|
|
2167
|
-
ctx.plugin(
|
|
2673
|
+
ctx.plugin(MemesLunaService, config);
|
|
2168
2674
|
ctx.inject(["memesluna", "server"], async (ctx2) => {
|
|
2169
2675
|
const service = ctx2.memesluna;
|
|
2170
2676
|
await service.ready;
|
|
@@ -2216,11 +2722,12 @@ function apply(ctx, config) {
|
|
|
2216
2722
|
});
|
|
2217
2723
|
}
|
|
2218
2724
|
}
|
|
2219
|
-
|
|
2725
|
+
var inject = ["database", "chatluna", "server"];
|
|
2220
2726
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2221
2727
|
0 && (module.exports = {
|
|
2728
|
+
Config,
|
|
2729
|
+
MemesLunaService,
|
|
2222
2730
|
apply,
|
|
2223
2731
|
inject,
|
|
2224
|
-
|
|
2225
|
-
...require("./service")
|
|
2732
|
+
name
|
|
2226
2733
|
});
|