koishi-plugin-memesluna 0.2.5 → 0.2.6

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 +297 -0
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -1409,6 +1409,7 @@ function buildAdminHtml(basePath) {
1409
1409
  <ul class="navbar-nav me-auto">
1410
1410
  <li class="nav-item"><a class="nav-link" href="${basePath}/">首页</a></li>
1411
1411
  <li class="nav-item"><a class="nav-link active" href="${basePath}/admin">管理</a></li>
1412
+ <li class="nav-item"><a class="nav-link" href="${basePath}/admin/endpoint">端点</a></li>
1412
1413
  </ul>
1413
1414
  </div>
1414
1415
  </div>
@@ -1807,6 +1808,297 @@ function buildAdminHtml(basePath) {
1807
1808
  </html>`;
1808
1809
  }
1809
1810
  __name(buildAdminHtml, "buildAdminHtml");
1811
+ function buildAdminEndpointHtml(basePath) {
1812
+ return `<!doctype html>
1813
+ <html lang="zh-CN">
1814
+ <head>
1815
+ <meta charset="utf-8" />
1816
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1817
+ <title>图床转发 - 302 端点管理</title>
1818
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
1819
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css">
1820
+ <style>
1821
+ body {
1822
+ min-height: 100vh;
1823
+ margin: 0;
1824
+ color: #1f2937;
1825
+ background-color: #f4f6fb;
1826
+ background-image: url('/project_bg/default_background.jpg');
1827
+ background-size: cover;
1828
+ background-position: center;
1829
+ background-repeat: no-repeat;
1830
+ background-attachment: fixed;
1831
+ }
1832
+
1833
+ body::before {
1834
+ content: '';
1835
+ position: fixed;
1836
+ inset: 0;
1837
+ background-color: rgba(255, 255, 255, 0.78);
1838
+ z-index: -1;
1839
+ }
1840
+
1841
+ .acrylic-navbar {
1842
+ background-color: rgba(248, 249, 250, 0.68);
1843
+ -webkit-backdrop-filter: blur(12px) saturate(150%);
1844
+ backdrop-filter: blur(12px) saturate(150%);
1845
+ border-bottom: 1px solid rgba(0, 0, 0, 0.06);
1846
+ }
1847
+
1848
+ .layout-shell {
1849
+ max-width: 1320px;
1850
+ }
1851
+
1852
+ .panel {
1853
+ border-radius: 14px;
1854
+ border: 1px solid rgba(255, 255, 255, 0.5);
1855
+ background: rgba(255, 255, 255, 0.72);
1856
+ -webkit-backdrop-filter: blur(10px) saturate(130%);
1857
+ backdrop-filter: blur(10px) saturate(130%);
1858
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
1859
+ padding: 1rem;
1860
+ }
1861
+
1862
+ .sidebar-panel {
1863
+ position: sticky;
1864
+ top: 1rem;
1865
+ }
1866
+
1867
+ .code-text {
1868
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1869
+ font-size: 0.8rem;
1870
+ word-break: break-all;
1871
+ }
1872
+ </style>
1873
+ </head>
1874
+ <body>
1875
+ <nav class="navbar navbar-expand-lg navbar-light acrylic-navbar">
1876
+ <div class="container layout-shell">
1877
+ <a class="navbar-brand" href="${basePath}/">MemesLuna</a>
1878
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavEndpoint">
1879
+ <span class="navbar-toggler-icon"></span>
1880
+ </button>
1881
+ <div class="collapse navbar-collapse" id="navbarNavEndpoint">
1882
+ <ul class="navbar-nav me-auto">
1883
+ <li class="nav-item"><a class="nav-link" href="${basePath}/">首页</a></li>
1884
+ <li class="nav-item"><a class="nav-link" href="${basePath}/admin">管理</a></li>
1885
+ <li class="nav-item"><a class="nav-link active" href="${basePath}/admin/endpoint">端点</a></li>
1886
+ </ul>
1887
+ </div>
1888
+ </div>
1889
+ </nav>
1890
+
1891
+ <div class="container layout-shell mt-3 pb-4">
1892
+ <div class="row g-3">
1893
+ <div class="col-lg-3">
1894
+ <div class="panel sidebar-panel">
1895
+ <h5 class="mb-3">添加 / 编辑端点</h5>
1896
+ <input id="endpoint-name" class="form-control mb-2" placeholder="端点名称" />
1897
+ <input id="endpoint-description" class="form-control mb-2" placeholder="描述" />
1898
+ <input id="endpoint-url" class="form-control mb-2" placeholder="目标 URL" />
1899
+
1900
+ <div class="d-grid gap-2 mt-3">
1901
+ <button id="save-endpoint" class="btn btn-primary">创建</button>
1902
+ <button id="reset-endpoint" class="btn btn-outline-secondary">清空</button>
1903
+ </div>
1904
+ </div>
1905
+ </div>
1906
+
1907
+ <div class="col-lg-9">
1908
+ <div class="panel">
1909
+ <h4 class="mb-3">302 跳转端点管理</h4>
1910
+ <p class="text-muted mb-3">通过 <code>${basePath}/端点名称</code> 访问,自动 302 跳转到目标 URL。</p>
1911
+
1912
+ <div class="table-responsive">
1913
+ <table class="table table-sm align-middle">
1914
+ <thead>
1915
+ <tr>
1916
+ <th>名称</th>
1917
+ <th>描述</th>
1918
+ <th>目标 URL</th>
1919
+ <th>访问</th>
1920
+ <th></th>
1921
+ </tr>
1922
+ </thead>
1923
+ <tbody id="endpoint-table"></tbody>
1924
+ </table>
1925
+ </div>
1926
+ </div>
1927
+ </div>
1928
+ </div>
1929
+
1930
+ <div id="endpoint-alert" class="alert mt-3 d-none"></div>
1931
+ </div>
1932
+
1933
+ <script>
1934
+ var BASE_PATH = '${basePath}'
1935
+ var endpointState = {
1936
+ endpoints: [],
1937
+ editingName: '',
1938
+ }
1939
+
1940
+ var byId = function(id) { return document.getElementById(id) }
1941
+
1942
+ function showAlert(message, type) {
1943
+ type = type || 'info'
1944
+ var el = byId('endpoint-alert')
1945
+ el.className = 'alert alert-' + type + ' mt-3'
1946
+ el.textContent = message
1947
+ el.classList.remove('d-none')
1948
+ setTimeout(function() { el.classList.add('d-none') }, 2400)
1949
+ }
1950
+
1951
+ async function request(url, options) {
1952
+ options = options || {}
1953
+ var headers = Object.assign({}, options.headers || {})
1954
+ if (options.body && !headers['Content-Type']) {
1955
+ headers['Content-Type'] = 'application/json'
1956
+ }
1957
+ var res = await fetch(url, Object.assign({}, options, { headers: headers }))
1958
+ var data = null
1959
+ try { data = await res.json() } catch(e) { data = null }
1960
+ if (!res.ok) {
1961
+ throw new Error(data && data.error ? data.error : String(res.status) + ' ' + String(res.statusText))
1962
+ }
1963
+ return data
1964
+ }
1965
+
1966
+ function resetForm() {
1967
+ endpointState.editingName = ''
1968
+ byId('endpoint-name').value = ''
1969
+ byId('endpoint-description').value = ''
1970
+ byId('endpoint-url').value = ''
1971
+ byId('save-endpoint').textContent = '创建'
1972
+ byId('endpoint-name').disabled = false
1973
+ }
1974
+
1975
+ function fillForm(item) {
1976
+ endpointState.editingName = item.name || ''
1977
+ byId('endpoint-name').value = item.name || ''
1978
+ byId('endpoint-description').value = item.description || ''
1979
+ byId('endpoint-url').value = item.url || ''
1980
+ byId('save-endpoint').textContent = '更新'
1981
+ byId('endpoint-name').disabled = true
1982
+ }
1983
+
1984
+ function renderTable() {
1985
+ var body = byId('endpoint-table')
1986
+ body.textContent = ''
1987
+
1988
+ if (!endpointState.endpoints.length) {
1989
+ var tr = document.createElement('tr')
1990
+ var td = document.createElement('td')
1991
+ td.colSpan = 5
1992
+ td.className = 'text-muted'
1993
+ td.textContent = '暂无端点'
1994
+ tr.appendChild(td)
1995
+ body.appendChild(tr)
1996
+ return
1997
+ }
1998
+
1999
+ endpointState.endpoints.forEach(function(item) {
2000
+ var tr = document.createElement('tr')
2001
+ var visitUrl = BASE_PATH + '/' + encodeURIComponent(item.name || '')
2002
+
2003
+ tr.innerHTML =
2004
+ '<td class="code-text"></td>' +
2005
+ '<td></td>' +
2006
+ '<td class="code-text"></td>' +
2007
+ '<td></td>' +
2008
+ '<td></td>'
2009
+
2010
+ tr.children[0].textContent = item.name || ''
2011
+ tr.children[1].textContent = item.description || '-'
2012
+ tr.children[2].textContent = item.url || ''
2013
+
2014
+ var visitLink = document.createElement('a')
2015
+ visitLink.href = visitUrl
2016
+ visitLink.target = '_blank'
2017
+ visitLink.className = 'btn btn-sm btn-outline-secondary'
2018
+ visitLink.innerHTML = '<i class="bi bi-box-arrow-up-right"></i>'
2019
+ tr.children[3].appendChild(visitLink)
2020
+
2021
+ var actionWrap = document.createElement('div')
2022
+ actionWrap.className = 'd-flex gap-1 justify-content-end'
2023
+
2024
+ var editBtn = document.createElement('button')
2025
+ editBtn.className = 'btn btn-sm btn-outline-primary'
2026
+ editBtn.textContent = '编辑'
2027
+ editBtn.addEventListener('click', function() { fillForm(item) })
2028
+
2029
+ var delBtn = document.createElement('button')
2030
+ delBtn.className = 'btn btn-sm btn-outline-danger'
2031
+ delBtn.textContent = '删除'
2032
+ delBtn.addEventListener('click', async function() {
2033
+ await request(BASE_PATH + '/api/admin/endpoints/' + encodeURIComponent(item.name || ''), {
2034
+ method: 'DELETE',
2035
+ })
2036
+ showAlert('端点已删除', 'success')
2037
+ await loadEndpoints()
2038
+ if (endpointState.editingName === item.name) resetForm()
2039
+ })
2040
+
2041
+ actionWrap.appendChild(editBtn)
2042
+ actionWrap.appendChild(delBtn)
2043
+ tr.children[4].appendChild(actionWrap)
2044
+ body.appendChild(tr)
2045
+ })
2046
+ }
2047
+
2048
+ async function loadEndpoints() {
2049
+ var data = await request(BASE_PATH + '/api/admin/endpoints')
2050
+ endpointState.endpoints = Array.isArray(data.endpoints) ? data.endpoints : []
2051
+ renderTable()
2052
+ }
2053
+
2054
+ byId('save-endpoint').addEventListener('click', async function() {
2055
+ var name = byId('endpoint-name').value.trim()
2056
+ var url = byId('endpoint-url').value.trim()
2057
+ if (!name || !url) {
2058
+ showAlert('名称与目标 URL 必填', 'warning')
2059
+ return
2060
+ }
2061
+
2062
+ var payload = {
2063
+ name: name,
2064
+ description: byId('endpoint-description').value.trim(),
2065
+ url: url,
2066
+ method: 'redirect',
2067
+ urlConstruction: 'normal',
2068
+ modelName: '',
2069
+ queryParams: [],
2070
+ proxySettings: { fallbackAction: 'returnJson' },
2071
+ }
2072
+
2073
+ if (endpointState.editingName) {
2074
+ await request(BASE_PATH + '/api/admin/endpoints/' + encodeURIComponent(endpointState.editingName), {
2075
+ method: 'PATCH',
2076
+ body: JSON.stringify(payload),
2077
+ })
2078
+ showAlert('端点更新成功', 'success')
2079
+ } else {
2080
+ await request(BASE_PATH + '/api/admin/endpoints', {
2081
+ method: 'POST',
2082
+ body: JSON.stringify(payload),
2083
+ })
2084
+ showAlert('端点创建成功', 'success')
2085
+ }
2086
+
2087
+ await loadEndpoints()
2088
+ resetForm()
2089
+ })
2090
+
2091
+ byId('reset-endpoint').addEventListener('click', resetForm)
2092
+
2093
+ loadEndpoints().catch(function(error) {
2094
+ showAlert(error instanceof Error ? error.message : String(error), 'danger')
2095
+ })
2096
+ </script>
2097
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
2098
+ </body>
2099
+ </html>`;
2100
+ }
2101
+ __name(buildAdminEndpointHtml, "buildAdminEndpointHtml");
1810
2102
  async function updateMemesVariable(ctx, config, service) {
1811
2103
  const baseUrl = toAbsoluteBaseUrl(ctx, config);
1812
2104
  const inventory = await service.buildRouteInventory(config.backendPath);
@@ -2153,6 +2445,11 @@ function applyServer(ctx, config, service) {
2153
2445
  koa.set("Content-Type", "text/html; charset=utf-8");
2154
2446
  koa.body = buildAdminHtml(basePath);
2155
2447
  });
2448
+ ctx.server.get(`${basePath}/admin/endpoint`, async (koa) => {
2449
+ koa.status = 200;
2450
+ koa.set("Content-Type", "text/html; charset=utf-8");
2451
+ koa.body = buildAdminEndpointHtml(basePath);
2452
+ });
2156
2453
  ctx.server.get(`${basePath}/api/collections/:name/resources`, async (koa) => {
2157
2454
  const collectionName = koa.params.name;
2158
2455
  const images = await service.getCollectionImages(collectionName);
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.5",
4
+ "version": "0.2.6",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "types": "lib/index.d.ts",