koishi-plugin-memesluna 0.2.3 → 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/lib/index.js +1035 -2
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1087,6 +1087,9 @@ function buildHomepageHtml(basePath) {
|
|
|
1087
1087
|
<li class="nav-item">
|
|
1088
1088
|
<a class="nav-link" href="${basePath}/admin">\u7BA1\u7406</a>
|
|
1089
1089
|
</li>
|
|
1090
|
+
<li class="nav-item">
|
|
1091
|
+
<a class="nav-link" href="${basePath}/admin/endpoint">\u7AEF\u70B9</a>
|
|
1092
|
+
</li>
|
|
1090
1093
|
</ul>
|
|
1091
1094
|
</div>
|
|
1092
1095
|
</div>
|
|
@@ -1261,6 +1264,1032 @@ function buildHomepageHtml(basePath) {
|
|
|
1261
1264
|
</body>
|
|
1262
1265
|
</html>`;
|
|
1263
1266
|
}
|
|
1267
|
+
function buildAdminHtml(basePath) {
|
|
1268
|
+
return `<!doctype html>
|
|
1269
|
+
<html lang="zh-CN">
|
|
1270
|
+
<head>
|
|
1271
|
+
<meta charset="utf-8" />
|
|
1272
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1273
|
+
<title>\u56FE\u5E8A\u8F6C\u53D1 - \u7BA1\u7406</title>
|
|
1274
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
1275
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css">
|
|
1276
|
+
<style>
|
|
1277
|
+
body {
|
|
1278
|
+
min-height: 100vh;
|
|
1279
|
+
margin: 0;
|
|
1280
|
+
color: #1f2937;
|
|
1281
|
+
background-color: #f4f6fb;
|
|
1282
|
+
background-image: url('/project_bg/default_background.jpg');
|
|
1283
|
+
background-size: cover;
|
|
1284
|
+
background-position: center;
|
|
1285
|
+
background-repeat: no-repeat;
|
|
1286
|
+
background-attachment: fixed;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
body::before {
|
|
1290
|
+
content: '';
|
|
1291
|
+
position: fixed;
|
|
1292
|
+
inset: 0;
|
|
1293
|
+
background-color: rgba(255, 255, 255, 0.78);
|
|
1294
|
+
z-index: -1;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
.acrylic-navbar {
|
|
1298
|
+
background-color: rgba(248, 249, 250, 0.68);
|
|
1299
|
+
-webkit-backdrop-filter: blur(12px) saturate(150%);
|
|
1300
|
+
backdrop-filter: blur(12px) saturate(150%);
|
|
1301
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
.admin-shell {
|
|
1305
|
+
max-width: 1320px;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
.sidebar-panel,
|
|
1309
|
+
.main-panel,
|
|
1310
|
+
.sub-card {
|
|
1311
|
+
border-radius: 14px;
|
|
1312
|
+
border: 1px solid rgba(255, 255, 255, 0.5);
|
|
1313
|
+
background: rgba(255, 255, 255, 0.72);
|
|
1314
|
+
-webkit-backdrop-filter: blur(10px) saturate(130%);
|
|
1315
|
+
backdrop-filter: blur(10px) saturate(130%);
|
|
1316
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
.sidebar-panel {
|
|
1320
|
+
padding: 1rem;
|
|
1321
|
+
position: sticky;
|
|
1322
|
+
top: 1rem;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
.main-panel {
|
|
1326
|
+
padding: 1rem;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
.collection-item {
|
|
1330
|
+
cursor: pointer;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
.collection-item.active {
|
|
1334
|
+
background: rgba(59, 130, 246, 0.14);
|
|
1335
|
+
border-color: rgba(59, 130, 246, 0.35);
|
|
1336
|
+
color: #1d4ed8;
|
|
1337
|
+
font-weight: 700;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
.image-grid {
|
|
1341
|
+
display: grid;
|
|
1342
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
1343
|
+
gap: 0.8rem;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
.image-card {
|
|
1347
|
+
border-radius: 10px;
|
|
1348
|
+
overflow: hidden;
|
|
1349
|
+
border: 1px solid rgba(148, 163, 184, 0.35);
|
|
1350
|
+
background: rgba(255, 255, 255, 0.9);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
.image-card img {
|
|
1354
|
+
width: 100%;
|
|
1355
|
+
height: 140px;
|
|
1356
|
+
object-fit: cover;
|
|
1357
|
+
display: block;
|
|
1358
|
+
background: #f1f5f9;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.sub-card {
|
|
1362
|
+
padding: 0.9rem;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
.code-url {
|
|
1366
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
1367
|
+
font-size: 0.8rem;
|
|
1368
|
+
word-break: break-all;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
.section-title {
|
|
1372
|
+
font-size: 1rem;
|
|
1373
|
+
font-weight: 700;
|
|
1374
|
+
color: #334155;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
.empty-tip {
|
|
1378
|
+
color: #64748b;
|
|
1379
|
+
font-size: 0.9rem;
|
|
1380
|
+
}
|
|
1381
|
+
</style>
|
|
1382
|
+
</head>
|
|
1383
|
+
<body>
|
|
1384
|
+
<nav class="navbar navbar-expand-lg navbar-light acrylic-navbar">
|
|
1385
|
+
<div class="container admin-shell">
|
|
1386
|
+
<a class="navbar-brand" href="${basePath}/">MemesLuna</a>
|
|
1387
|
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAdmin">
|
|
1388
|
+
<span class="navbar-toggler-icon"></span>
|
|
1389
|
+
</button>
|
|
1390
|
+
<div class="collapse navbar-collapse" id="navbarNavAdmin">
|
|
1391
|
+
<ul class="navbar-nav me-auto">
|
|
1392
|
+
<li class="nav-item"><a class="nav-link" href="${basePath}/">\u9996\u9875</a></li>
|
|
1393
|
+
<li class="nav-item"><a class="nav-link active" href="${basePath}/admin">\u7BA1\u7406</a></li>
|
|
1394
|
+
<li class="nav-item"><a class="nav-link" href="${basePath}/admin/endpoint">\u7AEF\u70B9</a></li>
|
|
1395
|
+
</ul>
|
|
1396
|
+
</div>
|
|
1397
|
+
</div>
|
|
1398
|
+
</nav>
|
|
1399
|
+
|
|
1400
|
+
<div class="container admin-shell mt-3 pb-4">
|
|
1401
|
+
<div class="row g-3">
|
|
1402
|
+
<div class="col-lg-3">
|
|
1403
|
+
<div class="sidebar-panel">
|
|
1404
|
+
<h5 class="mb-3">\u5408\u96C6\u7BA1\u7406</h5>
|
|
1405
|
+
<div class="input-group mb-3">
|
|
1406
|
+
<input id="new-collection-name" class="form-control" placeholder="\u65B0\u5408\u96C6\u540D\u79F0" />
|
|
1407
|
+
<button id="create-collection" class="btn btn-primary">\u521B\u5EFA</button>
|
|
1408
|
+
</div>
|
|
1409
|
+
<div id="collection-list" class="list-group mb-3"></div>
|
|
1410
|
+
<button id="delete-collection" class="btn btn-outline-danger w-100" disabled>\u5220\u9664\u5F53\u524D\u5408\u96C6</button>
|
|
1411
|
+
|
|
1412
|
+
<hr>
|
|
1413
|
+
<h6 class="mb-2">\u5408\u96C6\u63CF\u8FF0</h6>
|
|
1414
|
+
<div class="input-group mb-3">
|
|
1415
|
+
<input id="collection-description" class="form-control" placeholder="\u4E3A\u5F53\u524D\u5408\u96C6\u6DFB\u52A0\u63CF\u8FF0" disabled />
|
|
1416
|
+
<button id="save-description" class="btn btn-primary" disabled>\u4FDD\u5B58</button>
|
|
1417
|
+
</div>
|
|
1418
|
+
|
|
1419
|
+
<hr>
|
|
1420
|
+
<h6 class="mb-2">\u5FEB\u6377\u4FE1\u606F</h6>
|
|
1421
|
+
<div class="small text-muted">
|
|
1422
|
+
<div>\u7BA1\u7406\u94FE\u63A5\uFF1A<code>${basePath}/admin</code></div>
|
|
1423
|
+
<div class="mt-1">\u968F\u673A\u8BBF\u95EE\uFF1A<code id="collection-random-url">-</code></div>
|
|
1424
|
+
</div>
|
|
1425
|
+
</div>
|
|
1426
|
+
</div>
|
|
1427
|
+
|
|
1428
|
+
<div class="col-lg-9">
|
|
1429
|
+
<div class="main-panel mb-3">
|
|
1430
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
1431
|
+
<h5 class="mb-0">\u5408\u96C6\u5185\u5BB9</h5>
|
|
1432
|
+
<span id="selected-collection-badge" class="badge bg-primary">\u672A\u9009\u62E9</span>
|
|
1433
|
+
</div>
|
|
1434
|
+
|
|
1435
|
+
<div class="row g-3 mt-1">
|
|
1436
|
+
<div class="col-md-6">
|
|
1437
|
+
<div class="sub-card h-100">
|
|
1438
|
+
<div class="section-title mb-2">\u4E0A\u4F20\u672C\u5730\u56FE\u7247</div>
|
|
1439
|
+
<input id="upload-files" type="file" class="form-control mb-2" multiple accept="image/*" />
|
|
1440
|
+
<button id="upload-images" class="btn btn-primary w-100" disabled>\u4E0A\u4F20\u5230\u5F53\u524D\u5408\u96C6</button>
|
|
1441
|
+
<div class="form-text">\u652F\u6301\u591A\u9009\u3002\u4E0A\u4F20\u540E\u81EA\u52A8\u5237\u65B0\u5217\u8868\u3002</div>
|
|
1442
|
+
</div>
|
|
1443
|
+
</div>
|
|
1444
|
+
<div class="col-md-6">
|
|
1445
|
+
<div class="sub-card h-100">
|
|
1446
|
+
<div class="section-title mb-2">\u6DFB\u52A0\u5916\u94FE</div>
|
|
1447
|
+
<textarea id="links-input" class="form-control mb-2" rows="4" placeholder="\u6BCF\u884C\u4E00\u4E2A http/https \u94FE\u63A5"></textarea>
|
|
1448
|
+
<button id="add-links" class="btn btn-primary w-100" disabled>\u6DFB\u52A0\u5916\u94FE\u5230\u5F53\u524D\u5408\u96C6</button>
|
|
1449
|
+
</div>
|
|
1450
|
+
</div>
|
|
1451
|
+
</div>
|
|
1452
|
+
|
|
1453
|
+
<div class="sub-card mt-3">
|
|
1454
|
+
<div class="section-title mb-2">\u672C\u5730\u56FE\u7247</div>
|
|
1455
|
+
<div id="images-grid" class="image-grid"></div>
|
|
1456
|
+
<div id="images-empty" class="empty-tip mt-2">\u6682\u65E0\u672C\u5730\u56FE\u7247</div>
|
|
1457
|
+
</div>
|
|
1458
|
+
|
|
1459
|
+
<div class="sub-card mt-3">
|
|
1460
|
+
<div class="section-title mb-2">\u5916\u94FE\u5217\u8868</div>
|
|
1461
|
+
<div id="links-list" class="list-group"></div>
|
|
1462
|
+
<div id="links-empty" class="empty-tip mt-2">\u6682\u65E0\u5916\u94FE</div>
|
|
1463
|
+
</div>
|
|
1464
|
+
</div>
|
|
1465
|
+
|
|
1466
|
+
<div class="main-panel">
|
|
1467
|
+
<h5 class="mb-3">API \u7AEF\u70B9\u7BA1\u7406</h5>
|
|
1468
|
+
|
|
1469
|
+
<div class="row g-2 mb-3">
|
|
1470
|
+
<div class="col-md-3"><input id="endpoint-name" class="form-control" placeholder="name" /></div>
|
|
1471
|
+
<div class="col-md-3"><input id="endpoint-group" class="form-control" placeholder="group" /></div>
|
|
1472
|
+
<div class="col-md-3"><select id="endpoint-method" class="form-select"><option value="redirect">redirect</option><option value="proxy">proxy</option></select></div>
|
|
1473
|
+
<div class="col-md-3"><select id="endpoint-mode" class="form-select"><option value="normal">normal</option><option value="special_forward">special_forward</option><option value="special_pollinations">special_pollinations</option><option value="special_draw_redirect">special_draw_redirect</option></select></div>
|
|
1474
|
+
<div class="col-md-8"><input id="endpoint-url" class="form-control" placeholder="target url" /></div>
|
|
1475
|
+
<div class="col-md-4"><input id="endpoint-model" class="form-control" placeholder="modelName (optional)" /></div>
|
|
1476
|
+
<div class="col-md-6"><textarea id="endpoint-description" class="form-control" rows="2" placeholder="description"></textarea></div>
|
|
1477
|
+
<div class="col-md-3"><textarea id="endpoint-query" class="form-control code-url" rows="2" placeholder='queryParams JSON'></textarea></div>
|
|
1478
|
+
<div class="col-md-3"><textarea id="endpoint-proxy" class="form-control code-url" rows="2" placeholder='proxySettings JSON'></textarea></div>
|
|
1479
|
+
</div>
|
|
1480
|
+
|
|
1481
|
+
<div class="d-flex gap-2 mb-3">
|
|
1482
|
+
<button id="save-endpoint" class="btn btn-primary">\u521B\u5EFA / \u66F4\u65B0\u7AEF\u70B9</button>
|
|
1483
|
+
<button id="reset-endpoint" class="btn btn-outline-secondary">\u6E05\u7A7A\u8868\u5355</button>
|
|
1484
|
+
</div>
|
|
1485
|
+
|
|
1486
|
+
<div class="table-responsive">
|
|
1487
|
+
<table class="table table-sm align-middle">
|
|
1488
|
+
<thead>
|
|
1489
|
+
<tr>
|
|
1490
|
+
<th>name</th>
|
|
1491
|
+
<th>method</th>
|
|
1492
|
+
<th>mode</th>
|
|
1493
|
+
<th>url</th>
|
|
1494
|
+
<th></th>
|
|
1495
|
+
</tr>
|
|
1496
|
+
</thead>
|
|
1497
|
+
<tbody id="endpoint-table"></tbody>
|
|
1498
|
+
</table>
|
|
1499
|
+
</div>
|
|
1500
|
+
</div>
|
|
1501
|
+
</div>
|
|
1502
|
+
</div>
|
|
1503
|
+
|
|
1504
|
+
<div id="admin-alert" class="alert mt-3 d-none"></div>
|
|
1505
|
+
</div>
|
|
1506
|
+
|
|
1507
|
+
<script>
|
|
1508
|
+
const BASE_PATH = '${basePath}'
|
|
1509
|
+
const state = {
|
|
1510
|
+
collectionNames: [],
|
|
1511
|
+
collections: [],
|
|
1512
|
+
endpoints: [],
|
|
1513
|
+
selectedCollection: '',
|
|
1514
|
+
images: [],
|
|
1515
|
+
links: [],
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const byId = (id) => document.getElementById(id)
|
|
1519
|
+
|
|
1520
|
+
function showAlert(message, type = 'info') {
|
|
1521
|
+
const el = byId('admin-alert')
|
|
1522
|
+
el.className = 'alert alert-' + type + ' mt-3'
|
|
1523
|
+
el.textContent = message
|
|
1524
|
+
el.classList.remove('d-none')
|
|
1525
|
+
setTimeout(() => el.classList.add('d-none'), 2200)
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
async function request(url, options = {}) {
|
|
1529
|
+
const headers = Object.assign({}, options.headers || {})
|
|
1530
|
+
if (options.body && !headers['Content-Type']) {
|
|
1531
|
+
headers['Content-Type'] = 'application/json'
|
|
1532
|
+
}
|
|
1533
|
+
const res = await fetch(url, Object.assign({}, options, { headers }))
|
|
1534
|
+
let data = null
|
|
1535
|
+
try {
|
|
1536
|
+
data = await res.json()
|
|
1537
|
+
} catch {
|
|
1538
|
+
data = null
|
|
1539
|
+
}
|
|
1540
|
+
if (!res.ok) {
|
|
1541
|
+
throw new Error(data && data.error ? data.error : String(res.status) + ' ' + String(res.statusText))
|
|
1542
|
+
}
|
|
1543
|
+
return data
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
async function refreshState() {
|
|
1547
|
+
const data = await request(BASE_PATH + '/api/admin/state')
|
|
1548
|
+
state.collectionNames = Array.isArray(data.collectionNames) ? data.collectionNames : []
|
|
1549
|
+
state.collections = Array.isArray(data.collections) ? data.collections : []
|
|
1550
|
+
state.endpoints = Array.isArray(data.endpoints) ? data.endpoints : []
|
|
1551
|
+
|
|
1552
|
+
if (!state.selectedCollection || !state.collectionNames.includes(state.selectedCollection)) {
|
|
1553
|
+
state.selectedCollection = state.collectionNames[0] || ''
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
renderCollectionList()
|
|
1557
|
+
renderEndpointTable()
|
|
1558
|
+
await refreshCollectionResources()
|
|
1559
|
+
syncSelectedCollectionUi()
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
async function refreshCollectionResources() {
|
|
1563
|
+
if (!state.selectedCollection) {
|
|
1564
|
+
state.images = []
|
|
1565
|
+
state.links = []
|
|
1566
|
+
renderImages()
|
|
1567
|
+
renderLinks()
|
|
1568
|
+
return
|
|
1569
|
+
}
|
|
1570
|
+
const data = await request(BASE_PATH + '/api/collections/' + encodeURIComponent(state.selectedCollection) + '/resources')
|
|
1571
|
+
state.images = Array.isArray(data.images) ? data.images : []
|
|
1572
|
+
state.links = Array.isArray(data.links) ? data.links : []
|
|
1573
|
+
renderImages()
|
|
1574
|
+
renderLinks()
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function syncSelectedCollectionUi() {
|
|
1578
|
+
const selected = state.selectedCollection
|
|
1579
|
+
byId('selected-collection-badge').textContent = selected || '\u672A\u9009\u62E9'
|
|
1580
|
+
byId('delete-collection').disabled = !selected
|
|
1581
|
+
byId('upload-images').disabled = !selected
|
|
1582
|
+
byId('add-links').disabled = !selected
|
|
1583
|
+
byId('collection-random-url').textContent = selected ? BASE_PATH + '/' + selected : '-'
|
|
1584
|
+
byId('collection-description').disabled = !selected
|
|
1585
|
+
byId('save-description').disabled = !selected
|
|
1586
|
+
|
|
1587
|
+
const collectionInfo = state.collections.find((c) => c.name === selected)
|
|
1588
|
+
byId('collection-description').value = collectionInfo ? (collectionInfo.description || '') : ''
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function renderCollectionList() {
|
|
1592
|
+
const list = byId('collection-list')
|
|
1593
|
+
list.textContent = ''
|
|
1594
|
+
if (!state.collectionNames.length) {
|
|
1595
|
+
const empty = document.createElement('div')
|
|
1596
|
+
empty.className = 'text-muted small'
|
|
1597
|
+
empty.textContent = '\u6682\u65E0\u5408\u96C6'
|
|
1598
|
+
list.appendChild(empty)
|
|
1599
|
+
return
|
|
1600
|
+
}
|
|
1601
|
+
state.collectionNames.forEach((name) => {
|
|
1602
|
+
const button = document.createElement('button')
|
|
1603
|
+
button.type = 'button'
|
|
1604
|
+
button.className = 'list-group-item list-group-item-action collection-item' + (name === state.selectedCollection ? ' active' : '')
|
|
1605
|
+
button.textContent = name
|
|
1606
|
+
button.addEventListener('click', async () => {
|
|
1607
|
+
state.selectedCollection = name
|
|
1608
|
+
renderCollectionList()
|
|
1609
|
+
syncSelectedCollectionUi()
|
|
1610
|
+
await refreshCollectionResources()
|
|
1611
|
+
})
|
|
1612
|
+
list.appendChild(button)
|
|
1613
|
+
})
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function imagePreviewUrl(filename) {
|
|
1617
|
+
return BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images/' + encodeURIComponent(filename)
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function renderImages() {
|
|
1621
|
+
const grid = byId('images-grid')
|
|
1622
|
+
const empty = byId('images-empty')
|
|
1623
|
+
grid.textContent = ''
|
|
1624
|
+
empty.style.display = state.images.length ? 'none' : 'block'
|
|
1625
|
+
|
|
1626
|
+
state.images.forEach((name) => {
|
|
1627
|
+
const card = document.createElement('div')
|
|
1628
|
+
card.className = 'image-card'
|
|
1629
|
+
|
|
1630
|
+
const img = document.createElement('img')
|
|
1631
|
+
img.src = imagePreviewUrl(name)
|
|
1632
|
+
img.alt = name
|
|
1633
|
+
card.appendChild(img)
|
|
1634
|
+
|
|
1635
|
+
const body = document.createElement('div')
|
|
1636
|
+
body.className = 'p-2'
|
|
1637
|
+
const title = document.createElement('div')
|
|
1638
|
+
title.className = 'small text-truncate mb-2'
|
|
1639
|
+
title.textContent = name
|
|
1640
|
+
body.appendChild(title)
|
|
1641
|
+
|
|
1642
|
+
const row = document.createElement('div')
|
|
1643
|
+
row.className = 'd-flex gap-1'
|
|
1644
|
+
|
|
1645
|
+
const moveSelect = document.createElement('select')
|
|
1646
|
+
moveSelect.className = 'form-select form-select-sm'
|
|
1647
|
+
const collections = state.collectionNames.filter((item) => item !== state.selectedCollection)
|
|
1648
|
+
const placeholder = document.createElement('option')
|
|
1649
|
+
placeholder.value = ''
|
|
1650
|
+
placeholder.textContent = '\u79FB\u52A8\u5230...'
|
|
1651
|
+
moveSelect.appendChild(placeholder)
|
|
1652
|
+
collections.forEach((target) => {
|
|
1653
|
+
const opt = document.createElement('option')
|
|
1654
|
+
opt.value = target
|
|
1655
|
+
opt.textContent = target
|
|
1656
|
+
moveSelect.appendChild(opt)
|
|
1657
|
+
})
|
|
1658
|
+
|
|
1659
|
+
const moveBtn = document.createElement('button')
|
|
1660
|
+
moveBtn.className = 'btn btn-sm btn-outline-primary'
|
|
1661
|
+
moveBtn.textContent = '\u79FB\u52A8'
|
|
1662
|
+
moveBtn.disabled = collections.length === 0
|
|
1663
|
+
moveBtn.addEventListener('click', async () => {
|
|
1664
|
+
const targetCollection = moveSelect.value
|
|
1665
|
+
if (!targetCollection) return
|
|
1666
|
+
await request(
|
|
1667
|
+
BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images/' + encodeURIComponent(name) + '/move',
|
|
1668
|
+
{ method: 'POST', body: JSON.stringify({ targetCollection }) }
|
|
1669
|
+
)
|
|
1670
|
+
showAlert('\u56FE\u7247\u5DF2\u79FB\u52A8', 'success')
|
|
1671
|
+
await refreshState()
|
|
1672
|
+
})
|
|
1673
|
+
|
|
1674
|
+
const delBtn = document.createElement('button')
|
|
1675
|
+
delBtn.className = 'btn btn-sm btn-outline-danger'
|
|
1676
|
+
delBtn.textContent = '\u5220\u9664'
|
|
1677
|
+
delBtn.addEventListener('click', async () => {
|
|
1678
|
+
await request(
|
|
1679
|
+
BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images/' + encodeURIComponent(name),
|
|
1680
|
+
{ method: 'DELETE' }
|
|
1681
|
+
)
|
|
1682
|
+
showAlert('\u56FE\u7247\u5DF2\u5220\u9664', 'success')
|
|
1683
|
+
await refreshCollectionResources()
|
|
1684
|
+
})
|
|
1685
|
+
|
|
1686
|
+
row.appendChild(moveSelect)
|
|
1687
|
+
row.appendChild(moveBtn)
|
|
1688
|
+
row.appendChild(delBtn)
|
|
1689
|
+
body.appendChild(row)
|
|
1690
|
+
card.appendChild(body)
|
|
1691
|
+
grid.appendChild(card)
|
|
1692
|
+
})
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function renderLinks() {
|
|
1696
|
+
const wrap = byId('links-list')
|
|
1697
|
+
const empty = byId('links-empty')
|
|
1698
|
+
wrap.textContent = ''
|
|
1699
|
+
empty.style.display = state.links.length ? 'none' : 'block'
|
|
1700
|
+
state.links.forEach((link) => {
|
|
1701
|
+
const row = document.createElement('div')
|
|
1702
|
+
row.className = 'list-group-item d-flex justify-content-between align-items-center gap-2'
|
|
1703
|
+
|
|
1704
|
+
const text = document.createElement('div')
|
|
1705
|
+
text.className = 'code-url flex-grow-1'
|
|
1706
|
+
text.textContent = link
|
|
1707
|
+
|
|
1708
|
+
const actions = document.createElement('div')
|
|
1709
|
+
actions.className = 'd-flex gap-1'
|
|
1710
|
+
|
|
1711
|
+
const open = document.createElement('a')
|
|
1712
|
+
open.className = 'btn btn-sm btn-outline-primary'
|
|
1713
|
+
open.textContent = '\u67E5\u770B'
|
|
1714
|
+
open.href = link
|
|
1715
|
+
open.target = '_blank'
|
|
1716
|
+
|
|
1717
|
+
const del = document.createElement('button')
|
|
1718
|
+
del.className = 'btn btn-sm btn-outline-danger'
|
|
1719
|
+
del.textContent = '\u5220\u9664'
|
|
1720
|
+
del.addEventListener('click', async () => {
|
|
1721
|
+
await request(
|
|
1722
|
+
BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/links',
|
|
1723
|
+
{ method: 'DELETE', body: JSON.stringify({ link }) }
|
|
1724
|
+
)
|
|
1725
|
+
showAlert('\u5916\u94FE\u5DF2\u5220\u9664', 'success')
|
|
1726
|
+
await refreshCollectionResources()
|
|
1727
|
+
})
|
|
1728
|
+
|
|
1729
|
+
actions.appendChild(open)
|
|
1730
|
+
actions.appendChild(del)
|
|
1731
|
+
row.appendChild(text)
|
|
1732
|
+
row.appendChild(actions)
|
|
1733
|
+
wrap.appendChild(row)
|
|
1734
|
+
})
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function fillEndpointForm(item) {
|
|
1738
|
+
byId('endpoint-name').value = item.name || ''
|
|
1739
|
+
byId('endpoint-group').value = item.group || ''
|
|
1740
|
+
byId('endpoint-method').value = item.method || 'redirect'
|
|
1741
|
+
byId('endpoint-mode').value = item.urlConstruction || 'normal'
|
|
1742
|
+
byId('endpoint-url').value = item.url || ''
|
|
1743
|
+
byId('endpoint-model').value = item.modelName || ''
|
|
1744
|
+
byId('endpoint-description').value = item.description || ''
|
|
1745
|
+
byId('endpoint-query').value = JSON.stringify(item.queryParams || [], null, 2)
|
|
1746
|
+
byId('endpoint-proxy').value = JSON.stringify(item.proxySettings || {}, null, 2)
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
function renderEndpointTable() {
|
|
1750
|
+
const body = byId('endpoint-table')
|
|
1751
|
+
body.textContent = ''
|
|
1752
|
+
if (!state.endpoints.length) {
|
|
1753
|
+
const tr = document.createElement('tr')
|
|
1754
|
+
const td = document.createElement('td')
|
|
1755
|
+
td.colSpan = 5
|
|
1756
|
+
td.className = 'text-muted'
|
|
1757
|
+
td.textContent = '\u6682\u65E0 endpoint'
|
|
1758
|
+
tr.appendChild(td)
|
|
1759
|
+
body.appendChild(tr)
|
|
1760
|
+
return
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
state.endpoints.forEach((item) => {
|
|
1764
|
+
const tr = document.createElement('tr')
|
|
1765
|
+
tr.innerHTML =
|
|
1766
|
+
'<td class="code-url"></td>' +
|
|
1767
|
+
'<td></td>' +
|
|
1768
|
+
'<td class="code-url"></td>' +
|
|
1769
|
+
'<td class="code-url"></td>' +
|
|
1770
|
+
'<td></td>'
|
|
1771
|
+
|
|
1772
|
+
tr.children[0].textContent = item.name || ''
|
|
1773
|
+
tr.children[1].textContent = item.method || 'redirect'
|
|
1774
|
+
tr.children[2].textContent = item.urlConstruction || 'normal'
|
|
1775
|
+
tr.children[3].textContent = item.url || ''
|
|
1776
|
+
|
|
1777
|
+
const actions = document.createElement('div')
|
|
1778
|
+
actions.className = 'd-flex gap-1 justify-content-end'
|
|
1779
|
+
|
|
1780
|
+
const edit = document.createElement('button')
|
|
1781
|
+
edit.className = 'btn btn-sm btn-outline-primary'
|
|
1782
|
+
edit.textContent = '\u7F16\u8F91'
|
|
1783
|
+
edit.addEventListener('click', () => fillEndpointForm(item))
|
|
1784
|
+
|
|
1785
|
+
const del = document.createElement('button')
|
|
1786
|
+
del.className = 'btn btn-sm btn-outline-danger'
|
|
1787
|
+
del.textContent = '\u5220\u9664'
|
|
1788
|
+
del.addEventListener('click', async () => {
|
|
1789
|
+
await request(BASE_PATH + '/api/admin/endpoints/' + encodeURIComponent(item.name || ''), { method: 'DELETE' })
|
|
1790
|
+
showAlert('\u7AEF\u70B9\u5DF2\u5220\u9664', 'success')
|
|
1791
|
+
await refreshState()
|
|
1792
|
+
})
|
|
1793
|
+
|
|
1794
|
+
actions.appendChild(edit)
|
|
1795
|
+
actions.appendChild(del)
|
|
1796
|
+
tr.children[4].appendChild(actions)
|
|
1797
|
+
body.appendChild(tr)
|
|
1798
|
+
})
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function readFileAsDataURL(file) {
|
|
1802
|
+
return new Promise((resolve, reject) => {
|
|
1803
|
+
const reader = new FileReader()
|
|
1804
|
+
reader.onload = () => resolve(reader.result)
|
|
1805
|
+
reader.onerror = () => reject(new Error('\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25'))
|
|
1806
|
+
reader.readAsDataURL(file)
|
|
1807
|
+
})
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
byId('create-collection').addEventListener('click', async () => {
|
|
1811
|
+
const name = byId('new-collection-name').value.trim()
|
|
1812
|
+
if (!name) return
|
|
1813
|
+
await request(BASE_PATH + '/api/admin/collections', {
|
|
1814
|
+
method: 'POST',
|
|
1815
|
+
body: JSON.stringify({ name }),
|
|
1816
|
+
})
|
|
1817
|
+
byId('new-collection-name').value = ''
|
|
1818
|
+
showAlert('\u5408\u96C6\u521B\u5EFA\u6210\u529F', 'success')
|
|
1819
|
+
await refreshState()
|
|
1820
|
+
})
|
|
1821
|
+
|
|
1822
|
+
byId('delete-collection').addEventListener('click', async () => {
|
|
1823
|
+
if (!state.selectedCollection) return
|
|
1824
|
+
await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection), {
|
|
1825
|
+
method: 'DELETE',
|
|
1826
|
+
})
|
|
1827
|
+
showAlert('\u5408\u96C6\u5DF2\u5220\u9664', 'success')
|
|
1828
|
+
await refreshState()
|
|
1829
|
+
})
|
|
1830
|
+
|
|
1831
|
+
byId('save-description').addEventListener('click', async () => {
|
|
1832
|
+
if (!state.selectedCollection) return
|
|
1833
|
+
const description = byId('collection-description').value.trim()
|
|
1834
|
+
await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/description', {
|
|
1835
|
+
method: 'PATCH',
|
|
1836
|
+
body: JSON.stringify({ description }),
|
|
1837
|
+
})
|
|
1838
|
+
showAlert('\u63CF\u8FF0\u5DF2\u4FDD\u5B58', 'success')
|
|
1839
|
+
await refreshState()
|
|
1840
|
+
})
|
|
1841
|
+
|
|
1842
|
+
byId('upload-images').addEventListener('click', async () => {
|
|
1843
|
+
if (!state.selectedCollection) return
|
|
1844
|
+
const files = byId('upload-files').files
|
|
1845
|
+
if (!files || !files.length) return
|
|
1846
|
+
const images = []
|
|
1847
|
+
for (const file of files) {
|
|
1848
|
+
const base64 = await readFileAsDataURL(file)
|
|
1849
|
+
images.push({ base64, originalName: file.name })
|
|
1850
|
+
}
|
|
1851
|
+
await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images', {
|
|
1852
|
+
method: 'POST',
|
|
1853
|
+
body: JSON.stringify({ images }),
|
|
1854
|
+
})
|
|
1855
|
+
byId('upload-files').value = ''
|
|
1856
|
+
showAlert('\u56FE\u7247\u4E0A\u4F20\u6210\u529F', 'success')
|
|
1857
|
+
await refreshCollectionResources()
|
|
1858
|
+
await refreshState()
|
|
1859
|
+
})
|
|
1860
|
+
|
|
1861
|
+
byId('add-links').addEventListener('click', async () => {
|
|
1862
|
+
if (!state.selectedCollection) return
|
|
1863
|
+
const text = byId('links-input').value
|
|
1864
|
+
const links = text.split(/\r?
|
|
1865
|
+
/g).map((line) => line.trim()).filter(Boolean)
|
|
1866
|
+
if (!links.length) return
|
|
1867
|
+
await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/links', {
|
|
1868
|
+
method: 'POST',
|
|
1869
|
+
body: JSON.stringify({ links }),
|
|
1870
|
+
})
|
|
1871
|
+
byId('links-input').value = ''
|
|
1872
|
+
showAlert('\u5916\u94FE\u6DFB\u52A0\u6210\u529F', 'success')
|
|
1873
|
+
await refreshCollectionResources()
|
|
1874
|
+
await refreshState()
|
|
1875
|
+
})
|
|
1876
|
+
|
|
1877
|
+
byId('save-endpoint').addEventListener('click', async () => {
|
|
1878
|
+
const name = byId('endpoint-name').value.trim()
|
|
1879
|
+
const url = byId('endpoint-url').value.trim()
|
|
1880
|
+
if (!name || !url) {
|
|
1881
|
+
showAlert('name \u548C url \u5FC5\u586B', 'warning')
|
|
1882
|
+
return
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
let queryParams = []
|
|
1886
|
+
let proxySettings = {}
|
|
1887
|
+
try {
|
|
1888
|
+
queryParams = byId('endpoint-query').value.trim() ? JSON.parse(byId('endpoint-query').value) : []
|
|
1889
|
+
} catch {
|
|
1890
|
+
showAlert('queryParams JSON \u683C\u5F0F\u9519\u8BEF', 'warning')
|
|
1891
|
+
return
|
|
1892
|
+
}
|
|
1893
|
+
try {
|
|
1894
|
+
proxySettings = byId('endpoint-proxy').value.trim() ? JSON.parse(byId('endpoint-proxy').value) : {}
|
|
1895
|
+
} catch {
|
|
1896
|
+
showAlert('proxySettings JSON \u683C\u5F0F\u9519\u8BEF', 'warning')
|
|
1897
|
+
return
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
const payload = {
|
|
1901
|
+
name,
|
|
1902
|
+
group: byId('endpoint-group').value.trim(),
|
|
1903
|
+
description: byId('endpoint-description').value.trim(),
|
|
1904
|
+
url,
|
|
1905
|
+
method: byId('endpoint-method').value,
|
|
1906
|
+
urlConstruction: byId('endpoint-mode').value,
|
|
1907
|
+
modelName: byId('endpoint-model').value.trim(),
|
|
1908
|
+
queryParams,
|
|
1909
|
+
proxySettings,
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
const exists = state.endpoints.some((item) => item.name === name)
|
|
1913
|
+
if (exists) {
|
|
1914
|
+
await request(BASE_PATH + '/api/admin/endpoints/' + encodeURIComponent(name), {
|
|
1915
|
+
method: 'PATCH',
|
|
1916
|
+
body: JSON.stringify(payload),
|
|
1917
|
+
})
|
|
1918
|
+
showAlert('\u7AEF\u70B9\u5DF2\u66F4\u65B0', 'success')
|
|
1919
|
+
} else {
|
|
1920
|
+
await request(BASE_PATH + '/api/admin/endpoints', {
|
|
1921
|
+
method: 'POST',
|
|
1922
|
+
body: JSON.stringify(payload),
|
|
1923
|
+
})
|
|
1924
|
+
showAlert('\u7AEF\u70B9\u5DF2\u521B\u5EFA', 'success')
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
await refreshState()
|
|
1928
|
+
})
|
|
1929
|
+
|
|
1930
|
+
byId('reset-endpoint').addEventListener('click', () => {
|
|
1931
|
+
byId('endpoint-name').value = ''
|
|
1932
|
+
byId('endpoint-group').value = ''
|
|
1933
|
+
byId('endpoint-method').value = 'redirect'
|
|
1934
|
+
byId('endpoint-mode').value = 'normal'
|
|
1935
|
+
byId('endpoint-url').value = ''
|
|
1936
|
+
byId('endpoint-model').value = ''
|
|
1937
|
+
byId('endpoint-description').value = ''
|
|
1938
|
+
byId('endpoint-query').value = ''
|
|
1939
|
+
byId('endpoint-proxy').value = ''
|
|
1940
|
+
})
|
|
1941
|
+
|
|
1942
|
+
byId('upload-files').addEventListener('change', () => {
|
|
1943
|
+
byId('upload-images').disabled = !state.selectedCollection
|
|
1944
|
+
})
|
|
1945
|
+
|
|
1946
|
+
refreshState().catch((error) => {
|
|
1947
|
+
showAlert(error instanceof Error ? error.message : String(error), 'danger')
|
|
1948
|
+
})
|
|
1949
|
+
</script>
|
|
1950
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
1951
|
+
</body>
|
|
1952
|
+
</html>`;
|
|
1953
|
+
}
|
|
1954
|
+
function buildAdminEndpointHtml(basePath) {
|
|
1955
|
+
return `<!doctype html>
|
|
1956
|
+
<html lang="zh-CN">
|
|
1957
|
+
<head>
|
|
1958
|
+
<meta charset="utf-8" />
|
|
1959
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1960
|
+
<title>\u56FE\u5E8A\u8F6C\u53D1 - API \u7AEF\u70B9\u7BA1\u7406</title>
|
|
1961
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
1962
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css">
|
|
1963
|
+
<style>
|
|
1964
|
+
body {
|
|
1965
|
+
min-height: 100vh;
|
|
1966
|
+
margin: 0;
|
|
1967
|
+
color: #1f2937;
|
|
1968
|
+
background-color: #f4f6fb;
|
|
1969
|
+
background-image: url('/project_bg/default_background.jpg');
|
|
1970
|
+
background-size: cover;
|
|
1971
|
+
background-position: center;
|
|
1972
|
+
background-repeat: no-repeat;
|
|
1973
|
+
background-attachment: fixed;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
body::before {
|
|
1977
|
+
content: '';
|
|
1978
|
+
position: fixed;
|
|
1979
|
+
inset: 0;
|
|
1980
|
+
background-color: rgba(255, 255, 255, 0.78);
|
|
1981
|
+
z-index: -1;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
.acrylic-navbar {
|
|
1985
|
+
background-color: rgba(248, 249, 250, 0.68);
|
|
1986
|
+
-webkit-backdrop-filter: blur(12px) saturate(150%);
|
|
1987
|
+
backdrop-filter: blur(12px) saturate(150%);
|
|
1988
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
.layout-shell {
|
|
1992
|
+
max-width: 1320px;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
.panel {
|
|
1996
|
+
border-radius: 14px;
|
|
1997
|
+
border: 1px solid rgba(255, 255, 255, 0.5);
|
|
1998
|
+
background: rgba(255, 255, 255, 0.72);
|
|
1999
|
+
-webkit-backdrop-filter: blur(10px) saturate(130%);
|
|
2000
|
+
backdrop-filter: blur(10px) saturate(130%);
|
|
2001
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
2002
|
+
padding: 1rem;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
.sidebar-panel {
|
|
2006
|
+
position: sticky;
|
|
2007
|
+
top: 1rem;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
.code-text {
|
|
2011
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
2012
|
+
font-size: 0.8rem;
|
|
2013
|
+
word-break: break-all;
|
|
2014
|
+
}
|
|
2015
|
+
</style>
|
|
2016
|
+
</head>
|
|
2017
|
+
<body>
|
|
2018
|
+
<nav class="navbar navbar-expand-lg navbar-light acrylic-navbar">
|
|
2019
|
+
<div class="container layout-shell">
|
|
2020
|
+
<a class="navbar-brand" href="${basePath}/">MemesLuna</a>
|
|
2021
|
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavEndpoint">
|
|
2022
|
+
<span class="navbar-toggler-icon"></span>
|
|
2023
|
+
</button>
|
|
2024
|
+
<div class="collapse navbar-collapse" id="navbarNavEndpoint">
|
|
2025
|
+
<ul class="navbar-nav me-auto">
|
|
2026
|
+
<li class="nav-item"><a class="nav-link" href="${basePath}/">\u9996\u9875</a></li>
|
|
2027
|
+
<li class="nav-item"><a class="nav-link" href="${basePath}/admin">\u7BA1\u7406</a></li>
|
|
2028
|
+
<li class="nav-item"><a class="nav-link active" href="${basePath}/admin/endpoint">\u7AEF\u70B9</a></li>
|
|
2029
|
+
</ul>
|
|
2030
|
+
</div>
|
|
2031
|
+
</div>
|
|
2032
|
+
</nav>
|
|
2033
|
+
|
|
2034
|
+
<div class="container layout-shell mt-3 pb-4">
|
|
2035
|
+
<div class="row g-3">
|
|
2036
|
+
<div class="col-lg-3">
|
|
2037
|
+
<div class="panel sidebar-panel">
|
|
2038
|
+
<h5 class="mb-3">\u6DFB\u52A0 / \u7F16\u8F91\u7AEF\u70B9</h5>
|
|
2039
|
+
<input id="endpoint-name" class="form-control mb-2" placeholder="\u7AEF\u70B9\u540D\u79F0" />
|
|
2040
|
+
<input id="endpoint-description" class="form-control mb-2" placeholder="\u63CF\u8FF0" />
|
|
2041
|
+
<input id="endpoint-url" class="form-control mb-2" placeholder="\u76EE\u6807 URL" />
|
|
2042
|
+
<input id="endpoint-group" class="form-control mb-2" placeholder="\u5206\u7EC4" />
|
|
2043
|
+
<select id="endpoint-method" class="form-select mb-2">
|
|
2044
|
+
<option value="redirect">redirect</option>
|
|
2045
|
+
<option value="proxy">proxy</option>
|
|
2046
|
+
</select>
|
|
2047
|
+
<select id="endpoint-mode" class="form-select mb-2">
|
|
2048
|
+
<option value="normal">normal</option>
|
|
2049
|
+
<option value="special_forward">special_forward</option>
|
|
2050
|
+
<option value="special_pollinations">special_pollinations</option>
|
|
2051
|
+
<option value="special_draw_redirect">special_draw_redirect</option>
|
|
2052
|
+
</select>
|
|
2053
|
+
<input id="endpoint-model" class="form-control mb-2" placeholder="modelName (optional)" />
|
|
2054
|
+
<textarea id="endpoint-query" class="form-control code-text mb-2" rows="3" placeholder='queryParams JSON'></textarea>
|
|
2055
|
+
<textarea id="endpoint-proxy" class="form-control code-text mb-2" rows="3" placeholder='proxySettings JSON'></textarea>
|
|
2056
|
+
|
|
2057
|
+
<div class="d-grid gap-2">
|
|
2058
|
+
<button id="save-endpoint" class="btn btn-primary">\u521B\u5EFA</button>
|
|
2059
|
+
<button id="reset-endpoint" class="btn btn-outline-secondary">\u6E05\u7A7A</button>
|
|
2060
|
+
</div>
|
|
2061
|
+
</div>
|
|
2062
|
+
</div>
|
|
2063
|
+
|
|
2064
|
+
<div class="col-lg-9">
|
|
2065
|
+
<div class="panel">
|
|
2066
|
+
<h4 class="mb-3">API \u7AEF\u70B9\u7BA1\u7406</h4>
|
|
2067
|
+
<p class="text-muted mb-3">\u901A\u8FC7 <code>${basePath}/\u7AEF\u70B9\u540D\u79F0</code> \u8BBF\u95EE\u3002</p>
|
|
2068
|
+
|
|
2069
|
+
<div class="table-responsive">
|
|
2070
|
+
<table class="table table-sm align-middle">
|
|
2071
|
+
<thead>
|
|
2072
|
+
<tr>
|
|
2073
|
+
<th>\u540D\u79F0</th>
|
|
2074
|
+
<th>\u63CF\u8FF0</th>
|
|
2075
|
+
<th>\u6A21\u5F0F</th>
|
|
2076
|
+
<th>\u76EE\u6807 URL</th>
|
|
2077
|
+
<th></th>
|
|
2078
|
+
</tr>
|
|
2079
|
+
</thead>
|
|
2080
|
+
<tbody id="endpoint-table"></tbody>
|
|
2081
|
+
</table>
|
|
2082
|
+
</div>
|
|
2083
|
+
</div>
|
|
2084
|
+
</div>
|
|
2085
|
+
</div>
|
|
2086
|
+
|
|
2087
|
+
<div id="endpoint-alert" class="alert mt-3 d-none"></div>
|
|
2088
|
+
</div>
|
|
2089
|
+
|
|
2090
|
+
<script>
|
|
2091
|
+
const BASE_PATH = '${basePath}'
|
|
2092
|
+
const endpointState = {
|
|
2093
|
+
endpoints: [],
|
|
2094
|
+
editingName: '',
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
const byId = (id) => document.getElementById(id)
|
|
2098
|
+
|
|
2099
|
+
function showAlert(message, type = 'info') {
|
|
2100
|
+
const el = byId('endpoint-alert')
|
|
2101
|
+
el.className = 'alert alert-' + type + ' mt-3'
|
|
2102
|
+
el.textContent = message
|
|
2103
|
+
el.classList.remove('d-none')
|
|
2104
|
+
setTimeout(() => el.classList.add('d-none'), 2400)
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
async function request(url, options = {}) {
|
|
2108
|
+
const headers = Object.assign({}, options.headers || {})
|
|
2109
|
+
if (options.body && !headers['Content-Type']) {
|
|
2110
|
+
headers['Content-Type'] = 'application/json'
|
|
2111
|
+
}
|
|
2112
|
+
const res = await fetch(url, Object.assign({}, options, { headers }))
|
|
2113
|
+
let data = null
|
|
2114
|
+
try {
|
|
2115
|
+
data = await res.json()
|
|
2116
|
+
} catch {
|
|
2117
|
+
data = null
|
|
2118
|
+
}
|
|
2119
|
+
if (!res.ok) {
|
|
2120
|
+
throw new Error(data && data.error ? data.error : String(res.status) + ' ' + String(res.statusText))
|
|
2121
|
+
}
|
|
2122
|
+
return data
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
function resetForm() {
|
|
2126
|
+
endpointState.editingName = ''
|
|
2127
|
+
byId('endpoint-name').value = ''
|
|
2128
|
+
byId('endpoint-description').value = ''
|
|
2129
|
+
byId('endpoint-url').value = ''
|
|
2130
|
+
byId('endpoint-group').value = ''
|
|
2131
|
+
byId('endpoint-method').value = 'redirect'
|
|
2132
|
+
byId('endpoint-mode').value = 'normal'
|
|
2133
|
+
byId('endpoint-model').value = ''
|
|
2134
|
+
byId('endpoint-query').value = ''
|
|
2135
|
+
byId('endpoint-proxy').value = ''
|
|
2136
|
+
byId('save-endpoint').textContent = '\u521B\u5EFA'
|
|
2137
|
+
byId('endpoint-name').disabled = false
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
function fillForm(item) {
|
|
2141
|
+
endpointState.editingName = item.name || ''
|
|
2142
|
+
byId('endpoint-name').value = item.name || ''
|
|
2143
|
+
byId('endpoint-description').value = item.description || ''
|
|
2144
|
+
byId('endpoint-url').value = item.url || ''
|
|
2145
|
+
byId('endpoint-group').value = item.group || ''
|
|
2146
|
+
byId('endpoint-method').value = item.method || 'redirect'
|
|
2147
|
+
byId('endpoint-mode').value = item.urlConstruction || 'normal'
|
|
2148
|
+
byId('endpoint-model').value = item.modelName || ''
|
|
2149
|
+
byId('endpoint-query').value = JSON.stringify(item.queryParams || [], null, 2)
|
|
2150
|
+
byId('endpoint-proxy').value = JSON.stringify(item.proxySettings || {}, null, 2)
|
|
2151
|
+
byId('save-endpoint').textContent = '\u66F4\u65B0'
|
|
2152
|
+
byId('endpoint-name').disabled = true
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function renderTable() {
|
|
2156
|
+
const body = byId('endpoint-table')
|
|
2157
|
+
body.textContent = ''
|
|
2158
|
+
|
|
2159
|
+
if (!endpointState.endpoints.length) {
|
|
2160
|
+
const tr = document.createElement('tr')
|
|
2161
|
+
const td = document.createElement('td')
|
|
2162
|
+
td.colSpan = 5
|
|
2163
|
+
td.className = 'text-muted'
|
|
2164
|
+
td.textContent = '\u6682\u65E0\u7AEF\u70B9'
|
|
2165
|
+
tr.appendChild(td)
|
|
2166
|
+
body.appendChild(tr)
|
|
2167
|
+
return
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
endpointState.endpoints.forEach((item) => {
|
|
2171
|
+
const tr = document.createElement('tr')
|
|
2172
|
+
const visitUrl = BASE_PATH + '/' + encodeURIComponent(item.name || '')
|
|
2173
|
+
|
|
2174
|
+
tr.innerHTML =
|
|
2175
|
+
'<td class="code-text"></td>' +
|
|
2176
|
+
'<td></td>' +
|
|
2177
|
+
'<td class="code-text"></td>' +
|
|
2178
|
+
'<td class="code-text"></td>' +
|
|
2179
|
+
'<td></td>'
|
|
2180
|
+
|
|
2181
|
+
const link = document.createElement('a')
|
|
2182
|
+
link.href = visitUrl
|
|
2183
|
+
link.target = '_blank'
|
|
2184
|
+
link.className = 'text-decoration-none'
|
|
2185
|
+
link.textContent = '/' + (item.name || '')
|
|
2186
|
+
tr.children[0].appendChild(link)
|
|
2187
|
+
tr.children[1].textContent = item.description || '-'
|
|
2188
|
+
tr.children[2].textContent = (item.method || 'redirect') + ' \xB7 ' + (item.urlConstruction || 'normal')
|
|
2189
|
+
tr.children[3].textContent = item.url || ''
|
|
2190
|
+
|
|
2191
|
+
const actionWrap = document.createElement('div')
|
|
2192
|
+
actionWrap.className = 'd-flex gap-1 justify-content-end'
|
|
2193
|
+
|
|
2194
|
+
const editBtn = document.createElement('button')
|
|
2195
|
+
editBtn.className = 'btn btn-sm btn-outline-primary'
|
|
2196
|
+
editBtn.textContent = '\u7F16\u8F91'
|
|
2197
|
+
editBtn.addEventListener('click', () => fillForm(item))
|
|
2198
|
+
|
|
2199
|
+
const delBtn = document.createElement('button')
|
|
2200
|
+
delBtn.className = 'btn btn-sm btn-outline-danger'
|
|
2201
|
+
delBtn.textContent = '\u5220\u9664'
|
|
2202
|
+
delBtn.addEventListener('click', async () => {
|
|
2203
|
+
await request(BASE_PATH + '/api/admin/endpoints/' + encodeURIComponent(item.name || ''), {
|
|
2204
|
+
method: 'DELETE',
|
|
2205
|
+
})
|
|
2206
|
+
showAlert('\u7AEF\u70B9\u5DF2\u5220\u9664', 'success')
|
|
2207
|
+
await loadEndpoints()
|
|
2208
|
+
if (endpointState.editingName === item.name) resetForm()
|
|
2209
|
+
})
|
|
2210
|
+
|
|
2211
|
+
actionWrap.appendChild(editBtn)
|
|
2212
|
+
actionWrap.appendChild(delBtn)
|
|
2213
|
+
tr.children[4].appendChild(actionWrap)
|
|
2214
|
+
body.appendChild(tr)
|
|
2215
|
+
})
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
async function loadEndpoints() {
|
|
2219
|
+
const data = await request(BASE_PATH + '/api/admin/endpoints')
|
|
2220
|
+
endpointState.endpoints = Array.isArray(data.endpoints) ? data.endpoints : []
|
|
2221
|
+
renderTable()
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
byId('save-endpoint').addEventListener('click', async () => {
|
|
2225
|
+
const name = byId('endpoint-name').value.trim()
|
|
2226
|
+
const url = byId('endpoint-url').value.trim()
|
|
2227
|
+
if (!name || !url) {
|
|
2228
|
+
showAlert('name \u4E0E url \u5FC5\u586B', 'warning')
|
|
2229
|
+
return
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
let queryParams = []
|
|
2233
|
+
let proxySettings = {}
|
|
2234
|
+
|
|
2235
|
+
try {
|
|
2236
|
+
queryParams = byId('endpoint-query').value.trim()
|
|
2237
|
+
? JSON.parse(byId('endpoint-query').value)
|
|
2238
|
+
: []
|
|
2239
|
+
} catch {
|
|
2240
|
+
showAlert('queryParams JSON \u683C\u5F0F\u9519\u8BEF', 'warning')
|
|
2241
|
+
return
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
try {
|
|
2245
|
+
proxySettings = byId('endpoint-proxy').value.trim()
|
|
2246
|
+
? JSON.parse(byId('endpoint-proxy').value)
|
|
2247
|
+
: {}
|
|
2248
|
+
} catch {
|
|
2249
|
+
showAlert('proxySettings JSON \u683C\u5F0F\u9519\u8BEF', 'warning')
|
|
2250
|
+
return
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
const payload = {
|
|
2254
|
+
name,
|
|
2255
|
+
description: byId('endpoint-description').value.trim(),
|
|
2256
|
+
url,
|
|
2257
|
+
group: byId('endpoint-group').value.trim(),
|
|
2258
|
+
method: byId('endpoint-method').value,
|
|
2259
|
+
urlConstruction: byId('endpoint-mode').value,
|
|
2260
|
+
modelName: byId('endpoint-model').value.trim(),
|
|
2261
|
+
queryParams,
|
|
2262
|
+
proxySettings,
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
if (endpointState.editingName) {
|
|
2266
|
+
await request(BASE_PATH + '/api/admin/endpoints/' + encodeURIComponent(endpointState.editingName), {
|
|
2267
|
+
method: 'PATCH',
|
|
2268
|
+
body: JSON.stringify(payload),
|
|
2269
|
+
})
|
|
2270
|
+
showAlert('\u7AEF\u70B9\u66F4\u65B0\u6210\u529F', 'success')
|
|
2271
|
+
} else {
|
|
2272
|
+
await request(BASE_PATH + '/api/admin/endpoints', {
|
|
2273
|
+
method: 'POST',
|
|
2274
|
+
body: JSON.stringify(payload),
|
|
2275
|
+
})
|
|
2276
|
+
showAlert('\u7AEF\u70B9\u521B\u5EFA\u6210\u529F', 'success')
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
await loadEndpoints()
|
|
2280
|
+
resetForm()
|
|
2281
|
+
})
|
|
2282
|
+
|
|
2283
|
+
byId('reset-endpoint').addEventListener('click', resetForm)
|
|
2284
|
+
|
|
2285
|
+
loadEndpoints().catch((error) => {
|
|
2286
|
+
showAlert(error instanceof Error ? error.message : String(error), 'danger')
|
|
2287
|
+
})
|
|
2288
|
+
</script>
|
|
2289
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
2290
|
+
</body>
|
|
2291
|
+
</html>`;
|
|
2292
|
+
}
|
|
1264
2293
|
async function updateMemesVariable(ctx, config, service) {
|
|
1265
2294
|
const baseUrl = toAbsoluteBaseUrl(ctx, config);
|
|
1266
2295
|
const inventory = await service.buildRouteInventory(config.backendPath);
|
|
@@ -1599,10 +2628,14 @@ function applyServer(ctx, config, service) {
|
|
|
1599
2628
|
koa.body = { ok: true };
|
|
1600
2629
|
});
|
|
1601
2630
|
ctx.server.get(`${basePath}/admin`, async (koa) => {
|
|
1602
|
-
koa.
|
|
2631
|
+
koa.status = 200;
|
|
2632
|
+
koa.set("Content-Type", "text/html; charset=utf-8");
|
|
2633
|
+
koa.body = buildAdminHtml(basePath);
|
|
1603
2634
|
});
|
|
1604
2635
|
ctx.server.get(`${basePath}/admin/endpoint`, async (koa) => {
|
|
1605
|
-
koa.
|
|
2636
|
+
koa.status = 200;
|
|
2637
|
+
koa.set("Content-Type", "text/html; charset=utf-8");
|
|
2638
|
+
koa.body = buildAdminEndpointHtml(basePath);
|
|
1606
2639
|
});
|
|
1607
2640
|
ctx.server.get(`${basePath}/api/collections/:name/resources`, async (koa) => {
|
|
1608
2641
|
const collectionName = koa.params.name;
|
package/package.json
CHANGED