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.
Files changed (2) hide show
  1. package/lib/index.js +1035 -2
  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.redirect(`${basePath}/#admin`);
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.redirect(`${basePath}/#endpoint`);
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-memesluna",
3
3
  "description": "Image Forward service for Koishi with ChatLuna integration",
4
- "version": "0.2.3",
4
+ "version": "0.2.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "types": "lib/index.d.ts",