alemonjs 2.1.83-alpha.7 → 2.1.83-alpha.8
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.
|
@@ -1,31 +1,390 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
const escapeHtml = (value) => String(value)
|
|
2
|
+
.replace(/&/g, '&')
|
|
3
|
+
.replace(/</g, '<')
|
|
4
|
+
.replace(/>/g, '>')
|
|
5
|
+
.replace(/"/g, '"')
|
|
6
|
+
.replace(/'/g, ''');
|
|
7
|
+
const appHref = (app) => {
|
|
8
|
+
return app.kind === 'main' ? '/app' : `/apps/${app.name}`;
|
|
9
|
+
};
|
|
10
|
+
const appTags = (app) => {
|
|
11
|
+
const tags = [app.kind === 'main' ? '主应用' : '插件'];
|
|
12
|
+
if (app.capabilities.web) {
|
|
13
|
+
tags.push('页面');
|
|
14
|
+
}
|
|
15
|
+
if (app.capabilities.httpApi) {
|
|
16
|
+
tags.push('接口');
|
|
17
|
+
}
|
|
18
|
+
if (app.capabilities.event) {
|
|
19
|
+
tags.push('事件');
|
|
20
|
+
}
|
|
21
|
+
if (app.capabilities.expose) {
|
|
22
|
+
tags.push('Expose');
|
|
23
|
+
}
|
|
24
|
+
return tags;
|
|
25
|
+
};
|
|
26
|
+
const renderCard = (app) => {
|
|
27
|
+
const href = appHref(app);
|
|
28
|
+
const tags = appTags(app)
|
|
29
|
+
.map(tag => `<span class="app-tag">${escapeHtml(tag)}</span>`)
|
|
30
|
+
.join('');
|
|
31
|
+
const desc = app.kind === 'main'
|
|
32
|
+
? '访问主应用页面、接口与默认资源。'
|
|
33
|
+
: '访问插件页面、接口与公开入口。';
|
|
34
|
+
return `
|
|
35
|
+
<a
|
|
36
|
+
class="app-card"
|
|
37
|
+
href="${escapeHtml(href)}"
|
|
38
|
+
data-app-id="${escapeHtml(app.name)}"
|
|
39
|
+
data-app-kind="${escapeHtml(app.kind)}"
|
|
40
|
+
data-app-href="${escapeHtml(href)}"
|
|
41
|
+
>
|
|
42
|
+
<div class="app-card__top">
|
|
43
|
+
<div>
|
|
44
|
+
<p class="app-eyebrow">${app.kind === 'main' ? 'Main App' : 'Plugin App'}</p>
|
|
45
|
+
<h2 class="app-title">${escapeHtml(app.name)}</h2>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="app-rank">
|
|
48
|
+
<span class="app-rank__label">热度</span>
|
|
49
|
+
<span class="app-rank__value" data-click-count>0</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<p class="app-desc">${escapeHtml(desc)}</p>
|
|
53
|
+
<div class="app-tags">${tags}</div>
|
|
54
|
+
<div class="app-card__bottom">
|
|
55
|
+
<span class="app-link">${escapeHtml(href)}</span>
|
|
56
|
+
<span class="app-action">进入</span>
|
|
57
|
+
</div>
|
|
58
|
+
</a>
|
|
59
|
+
`;
|
|
60
|
+
};
|
|
61
|
+
const renderEmpty = () => {
|
|
62
|
+
return `
|
|
63
|
+
<div class="empty-state">
|
|
64
|
+
<p class="empty-state__title">当前没有可展示的应用。</p>
|
|
65
|
+
<p class="empty-state__desc">请先加载主应用或插件,再刷新本页查看入口。</p>
|
|
66
|
+
</div>
|
|
67
|
+
`;
|
|
68
|
+
};
|
|
69
|
+
const renderHelloHtml = (apps) => {
|
|
70
|
+
const visibleApps = apps
|
|
71
|
+
.filter(app => app.enabled && app.status === 'ready' && (app.capabilities.web || app.capabilities.httpApi))
|
|
72
|
+
.sort((left, right) => {
|
|
73
|
+
if (left.kind === 'main' && right.kind !== 'main') {
|
|
74
|
+
return -1;
|
|
75
|
+
}
|
|
76
|
+
if (left.kind !== 'main' && right.kind === 'main') {
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
return left.name.localeCompare(right.name);
|
|
80
|
+
});
|
|
81
|
+
const cards = visibleApps.length ? visibleApps.map(renderCard).join('') : renderEmpty();
|
|
82
|
+
const payload = JSON.stringify(visibleApps.map(app => ({
|
|
83
|
+
id: app.name,
|
|
84
|
+
href: appHref(app),
|
|
85
|
+
kind: app.kind
|
|
86
|
+
})));
|
|
87
|
+
return `<!DOCTYPE html>
|
|
88
|
+
<html lang="zh-CN">
|
|
89
|
+
<head>
|
|
90
|
+
<meta charset="utf-8" />
|
|
91
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
92
|
+
<title>ALemonJS 应用入口</title>
|
|
93
|
+
<style>
|
|
94
|
+
:root {
|
|
95
|
+
--bg: #f3efe5;
|
|
96
|
+
--panel: rgba(255, 251, 245, 0.88);
|
|
97
|
+
--panel-strong: #fffaf2;
|
|
98
|
+
--text: #17212b;
|
|
99
|
+
--muted: #6a7684;
|
|
100
|
+
--line: rgba(23, 33, 43, 0.08);
|
|
101
|
+
--accent: #d96c28;
|
|
102
|
+
--accent-strong: #a54a13;
|
|
103
|
+
--shadow: 0 24px 60px rgba(47, 35, 20, 0.12);
|
|
104
|
+
}
|
|
105
|
+
* { box-sizing: border-box; }
|
|
106
|
+
html, body { margin: 0; min-height: 100%; }
|
|
6
107
|
body {
|
|
7
|
-
|
|
108
|
+
font-family: "SF Pro Display", "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
109
|
+
color: var(--text);
|
|
110
|
+
background:
|
|
111
|
+
radial-gradient(circle at top left, rgba(217, 108, 40, 0.18), transparent 28%),
|
|
112
|
+
radial-gradient(circle at right center, rgba(78, 140, 124, 0.14), transparent 26%),
|
|
113
|
+
linear-gradient(180deg, #f8f4ec 0%, var(--bg) 100%);
|
|
114
|
+
}
|
|
115
|
+
.page {
|
|
116
|
+
width: min(1680px, calc(100vw - 32px));
|
|
8
117
|
margin: 0 auto;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
118
|
+
padding: clamp(28px, 4vw, 56px) 0 64px;
|
|
119
|
+
}
|
|
120
|
+
.hero {
|
|
121
|
+
position: relative;
|
|
122
|
+
overflow: hidden;
|
|
123
|
+
padding: clamp(28px, 4vw, 56px);
|
|
124
|
+
border-radius: 32px;
|
|
125
|
+
background: linear-gradient(135deg, rgba(255, 250, 242, 0.95), rgba(255, 244, 231, 0.88));
|
|
126
|
+
border: 1px solid rgba(255, 255, 255, 0.72);
|
|
127
|
+
box-shadow: var(--shadow);
|
|
128
|
+
}
|
|
129
|
+
.hero::after {
|
|
130
|
+
content: "";
|
|
131
|
+
position: absolute;
|
|
132
|
+
inset: auto -8% -48% auto;
|
|
133
|
+
width: min(38vw, 520px);
|
|
134
|
+
aspect-ratio: 1;
|
|
135
|
+
border-radius: 999px;
|
|
136
|
+
background: radial-gradient(circle, rgba(217, 108, 40, 0.18), transparent 62%);
|
|
137
|
+
pointer-events: none;
|
|
138
|
+
}
|
|
139
|
+
.hero-kicker {
|
|
140
|
+
margin: 0 0 12px;
|
|
141
|
+
font-size: clamp(14px, 1.2vw, 18px);
|
|
142
|
+
letter-spacing: 0.18em;
|
|
143
|
+
text-transform: uppercase;
|
|
144
|
+
color: var(--accent-strong);
|
|
145
|
+
}
|
|
146
|
+
.hero-title {
|
|
147
|
+
margin: 0;
|
|
148
|
+
max-width: 12ch;
|
|
149
|
+
font-size: clamp(40px, 7vw, 86px);
|
|
150
|
+
line-height: 0.95;
|
|
151
|
+
letter-spacing: -0.04em;
|
|
152
|
+
}
|
|
153
|
+
.hero-desc {
|
|
154
|
+
margin: 18px 0 0;
|
|
155
|
+
max-width: 56rem;
|
|
156
|
+
font-size: clamp(18px, 2vw, 28px);
|
|
157
|
+
line-height: 1.6;
|
|
158
|
+
color: var(--muted);
|
|
159
|
+
}
|
|
160
|
+
.hero-meta {
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-wrap: wrap;
|
|
163
|
+
gap: 14px;
|
|
164
|
+
margin-top: 28px;
|
|
165
|
+
}
|
|
166
|
+
.hero-pill {
|
|
167
|
+
display: inline-flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
gap: 10px;
|
|
170
|
+
padding: 12px 18px;
|
|
171
|
+
border-radius: 999px;
|
|
172
|
+
background: rgba(255, 255, 255, 0.78);
|
|
173
|
+
border: 1px solid rgba(23, 33, 43, 0.08);
|
|
174
|
+
font-size: clamp(14px, 1.4vw, 18px);
|
|
175
|
+
}
|
|
176
|
+
.hero-pill strong {
|
|
177
|
+
color: var(--accent-strong);
|
|
178
|
+
}
|
|
179
|
+
.section-head {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: end;
|
|
182
|
+
justify-content: space-between;
|
|
183
|
+
gap: 20px;
|
|
184
|
+
margin: 28px 0 18px;
|
|
185
|
+
padding: 0 6px;
|
|
186
|
+
}
|
|
187
|
+
.section-title {
|
|
188
|
+
margin: 0;
|
|
189
|
+
font-size: clamp(28px, 3vw, 42px);
|
|
190
|
+
}
|
|
191
|
+
.section-note {
|
|
192
|
+
margin: 0;
|
|
193
|
+
font-size: clamp(14px, 1.3vw, 18px);
|
|
194
|
+
color: var(--muted);
|
|
195
|
+
}
|
|
196
|
+
.app-grid {
|
|
197
|
+
display: grid;
|
|
198
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
|
|
199
|
+
gap: 18px;
|
|
200
|
+
}
|
|
201
|
+
.app-card {
|
|
202
|
+
display: flex;
|
|
203
|
+
flex-direction: column;
|
|
204
|
+
gap: 18px;
|
|
205
|
+
min-height: 280px;
|
|
206
|
+
padding: 24px;
|
|
207
|
+
border-radius: 28px;
|
|
208
|
+
text-decoration: none;
|
|
209
|
+
color: inherit;
|
|
210
|
+
background: var(--panel);
|
|
211
|
+
border: 1px solid var(--line);
|
|
212
|
+
box-shadow: 0 14px 40px rgba(30, 37, 44, 0.08);
|
|
213
|
+
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
|
214
|
+
}
|
|
215
|
+
.app-card:hover {
|
|
216
|
+
transform: translateY(-4px);
|
|
217
|
+
border-color: rgba(217, 108, 40, 0.28);
|
|
218
|
+
box-shadow: 0 24px 50px rgba(30, 37, 44, 0.12);
|
|
219
|
+
}
|
|
220
|
+
.app-card__top,
|
|
221
|
+
.app-card__bottom {
|
|
222
|
+
display: flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
justify-content: space-between;
|
|
225
|
+
gap: 16px;
|
|
226
|
+
}
|
|
227
|
+
.app-eyebrow {
|
|
228
|
+
margin: 0 0 8px;
|
|
229
|
+
font-size: 13px;
|
|
230
|
+
letter-spacing: 0.16em;
|
|
231
|
+
text-transform: uppercase;
|
|
232
|
+
color: var(--accent-strong);
|
|
233
|
+
}
|
|
234
|
+
.app-title {
|
|
235
|
+
margin: 0;
|
|
236
|
+
font-size: clamp(28px, 2.8vw, 38px);
|
|
237
|
+
line-height: 1;
|
|
238
|
+
}
|
|
239
|
+
.app-desc {
|
|
240
|
+
margin: 0;
|
|
241
|
+
font-size: clamp(16px, 1.4vw, 20px);
|
|
242
|
+
line-height: 1.7;
|
|
243
|
+
color: var(--muted);
|
|
244
|
+
}
|
|
245
|
+
.app-tags {
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-wrap: wrap;
|
|
248
|
+
gap: 10px;
|
|
249
|
+
}
|
|
250
|
+
.app-tag {
|
|
251
|
+
padding: 8px 12px;
|
|
252
|
+
border-radius: 999px;
|
|
253
|
+
background: rgba(217, 108, 40, 0.1);
|
|
254
|
+
color: var(--accent-strong);
|
|
255
|
+
font-size: 14px;
|
|
256
|
+
}
|
|
257
|
+
.app-rank {
|
|
258
|
+
min-width: 84px;
|
|
259
|
+
text-align: right;
|
|
260
|
+
}
|
|
261
|
+
.app-rank__label,
|
|
262
|
+
.app-link {
|
|
263
|
+
display: block;
|
|
264
|
+
font-size: 13px;
|
|
265
|
+
color: var(--muted);
|
|
266
|
+
}
|
|
267
|
+
.app-rank__value {
|
|
268
|
+
font-size: clamp(24px, 2vw, 30px);
|
|
269
|
+
font-weight: 700;
|
|
270
|
+
}
|
|
271
|
+
.app-action {
|
|
272
|
+
padding: 12px 18px;
|
|
273
|
+
border-radius: 999px;
|
|
274
|
+
background: #1d2d38;
|
|
275
|
+
color: #fff;
|
|
276
|
+
font-size: 15px;
|
|
277
|
+
}
|
|
278
|
+
.empty-state {
|
|
279
|
+
padding: 32px;
|
|
280
|
+
border-radius: 28px;
|
|
281
|
+
background: var(--panel-strong);
|
|
282
|
+
border: 1px dashed rgba(23, 33, 43, 0.16);
|
|
283
|
+
}
|
|
284
|
+
.empty-state__title {
|
|
285
|
+
margin: 0;
|
|
286
|
+
font-size: 24px;
|
|
287
|
+
}
|
|
288
|
+
.empty-state__desc {
|
|
289
|
+
margin: 10px 0 0;
|
|
290
|
+
color: var(--muted);
|
|
291
|
+
font-size: 16px;
|
|
292
|
+
}
|
|
293
|
+
@media (max-width: 820px) {
|
|
294
|
+
.page { width: min(100vw - 20px, 100%); padding-top: 18px; }
|
|
295
|
+
.hero { border-radius: 24px; padding: 22px; }
|
|
296
|
+
.section-head { align-items: start; flex-direction: column; }
|
|
297
|
+
.app-card { min-height: 0; padding: 20px; border-radius: 22px; }
|
|
298
|
+
.app-card__top,
|
|
299
|
+
.app-card__bottom { align-items: start; flex-direction: column; }
|
|
300
|
+
.app-rank { text-align: left; min-width: 0; }
|
|
301
|
+
}
|
|
302
|
+
</style>
|
|
303
|
+
</head>
|
|
304
|
+
<body>
|
|
305
|
+
<main class="page">
|
|
306
|
+
<section class="hero">
|
|
307
|
+
<p class="hero-kicker">ALemonJS Launchpad</p>
|
|
308
|
+
<h1 class="hero-title">欢迎使用阿柠檬机器人应用服务</h1>
|
|
309
|
+
<p class="hero-desc">这里会自动列出当前已准备好的主应用与插件。</p>
|
|
310
|
+
<div class="hero-meta">
|
|
311
|
+
<span class="hero-pill"><strong>${visibleApps.length}</strong> 个可访问入口</span>
|
|
312
|
+
<span class="hero-pill">主应用统一在 <strong>/app</strong></span>
|
|
313
|
+
<span class="hero-pill">插件统一在 <strong>/apps/<name></strong></span>
|
|
314
|
+
</div>
|
|
315
|
+
</section>
|
|
316
|
+
<section>
|
|
317
|
+
<div class="section-head">
|
|
318
|
+
<div>
|
|
319
|
+
<h2 class="section-title">应用列表</h2>
|
|
320
|
+
<p class="section-note">点击次数越高,卡片排序越靠前。排序记录仅保存在当前浏览器本地。</p>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
<div class="app-grid" id="app-grid">${cards}</div>
|
|
324
|
+
</section>
|
|
325
|
+
</main>
|
|
326
|
+
<script>
|
|
327
|
+
(() => {
|
|
328
|
+
const storageKey = 'alemonjs:launchpad:clicks';
|
|
329
|
+
const apps = ${payload};
|
|
330
|
+
const grid = document.getElementById('app-grid');
|
|
331
|
+
if (!grid) return;
|
|
332
|
+
|
|
333
|
+
const readClicks = () => {
|
|
334
|
+
try {
|
|
335
|
+
return JSON.parse(localStorage.getItem(storageKey) || '{}');
|
|
336
|
+
} catch {
|
|
337
|
+
return {};
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const writeClicks = (value) => {
|
|
342
|
+
localStorage.setItem(storageKey, JSON.stringify(value));
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const scoreOf = (id, clicks) => Number(clicks[id] || 0);
|
|
346
|
+
const clicks = readClicks();
|
|
347
|
+
const cards = Array.from(grid.querySelectorAll('[data-app-id]'));
|
|
348
|
+
|
|
349
|
+
const refreshCounts = () => {
|
|
350
|
+
cards.forEach((card) => {
|
|
351
|
+
const id = card.getAttribute('data-app-id') || '';
|
|
352
|
+
const node = card.querySelector('[data-click-count]');
|
|
353
|
+
if (node) {
|
|
354
|
+
node.textContent = String(scoreOf(id, clicks));
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const sortCards = () => {
|
|
360
|
+
cards
|
|
361
|
+
.sort((left, right) => {
|
|
362
|
+
const leftId = left.getAttribute('data-app-id') || '';
|
|
363
|
+
const rightId = right.getAttribute('data-app-id') || '';
|
|
364
|
+
const diff = scoreOf(rightId, clicks) - scoreOf(leftId, clicks);
|
|
365
|
+
if (diff !== 0) return diff;
|
|
366
|
+
return leftId.localeCompare(rightId);
|
|
367
|
+
})
|
|
368
|
+
.forEach(card => grid.appendChild(card));
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
cards.forEach((card) => {
|
|
372
|
+
card.addEventListener('click', () => {
|
|
373
|
+
const id = card.getAttribute('data-app-id') || '';
|
|
374
|
+
clicks[id] = scoreOf(id, clicks) + 1;
|
|
375
|
+
writeClicks(clicks);
|
|
376
|
+
refreshCounts();
|
|
377
|
+
sortCards();
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
refreshCounts();
|
|
382
|
+
sortCards();
|
|
383
|
+
})();
|
|
384
|
+
</script>
|
|
385
|
+
</body>
|
|
386
|
+
</html>`;
|
|
387
|
+
};
|
|
388
|
+
var hello_html = renderHelloHtml([]);
|
|
30
389
|
|
|
31
|
-
export {
|
|
390
|
+
export { hello_html as default, renderHelloHtml };
|
|
@@ -2,7 +2,7 @@ import KoaRouter from 'koa-router';
|
|
|
2
2
|
import fs__default, { existsSync } from 'fs';
|
|
3
3
|
import path__default, { join, dirname } from 'path';
|
|
4
4
|
import mime from 'mime-types';
|
|
5
|
-
import
|
|
5
|
+
import { renderHelloHtml } from './hello.html.js';
|
|
6
6
|
import { safePath, getModuelFile, formatPath, isValidPackageName } from './utils.js';
|
|
7
7
|
import { collectMiddlewares, runMiddlewares } from './middleware.js';
|
|
8
8
|
import module$1 from 'module';
|
|
@@ -155,9 +155,7 @@ const dispatchRegisteredKoaRouters = async (ctx) => {
|
|
|
155
155
|
}
|
|
156
156
|
const registerRouters = getChildrenApp(appName)?.register?.koaRouter;
|
|
157
157
|
const storedRouters = getRuntimeAppKoaRouters(appName);
|
|
158
|
-
const routers = (storedRouters.length
|
|
159
|
-
? storedRouters
|
|
160
|
-
: (Array.isArray(registerRouters) ? registerRouters : registerRouters ? [registerRouters] : [])).filter(Boolean);
|
|
158
|
+
const routers = (storedRouters.length ? storedRouters : Array.isArray(registerRouters) ? registerRouters : registerRouters ? [registerRouters] : []).filter(Boolean);
|
|
161
159
|
const aliasBases = appName === 'main' ? ['', '/app'] : ['', `/apps/${appName}`];
|
|
162
160
|
for (const koaRouter of routers) {
|
|
163
161
|
for (const basePath of aliasBases) {
|
|
@@ -228,7 +226,7 @@ const dispatchRegisteredKoaRouters = async (ctx) => {
|
|
|
228
226
|
router.get('/', ctx => {
|
|
229
227
|
ctx.status = 200;
|
|
230
228
|
ctx.set('Content-Type', 'text/html; charset=utf-8');
|
|
231
|
-
ctx.body =
|
|
229
|
+
ctx.body = renderHelloHtml(listRuntimeApps());
|
|
232
230
|
});
|
|
233
231
|
router.get('api/online', ctx => {
|
|
234
232
|
ctx.status = 200;
|