alemonjs 2.1.84 → 2.1.86

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.
@@ -689,7 +689,7 @@ class Router {
689
689
  const md = Format.createMarkdown();
690
690
  const format = Format.create();
691
691
  const routeEntry = (result.eventName && result.matchedPath
692
- ? (this.routes.get(result.eventName)?.one.get(result.matchedPath) ?? this.routes.get(result.eventName)?.two.get(result.matchedPath))
692
+ ? this.routes.get(result.eventName)?.one.get(result.matchedPath) ?? this.routes.get(result.eventName)?.two.get(result.matchedPath)
693
693
  : undefined) ?? undefined;
694
694
  const description = formatRouteDescription(routeEntry?.config.description);
695
695
  const schemaHints = buildSchemaHints(routeEntry?.config.schema);
@@ -1,29 +1,325 @@
1
- const escapeHtml = (value) => String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
2
- const appHref = (app) => {
3
- return app.kind === 'main' ? '/app' : `/apps/${app.name}`;
1
+ import { DOCTYPE, renderToString, createElement, Component, Html, Head, Title, Style, Body, Div, P } from '../../../../common/react.js';
2
+
3
+ const escapeHtml = (value) => {
4
+ return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
4
5
  };
5
- const appTags = (app) => {
6
- const tags = [app.kind === 'main' ? '主应用' : '插件'];
7
- if (app.capabilities.web) {
8
- tags.push('页面');
6
+ const styles = `
7
+ :root {
8
+ --bg: #ece5d6;
9
+ --bg-deep: #e4dac6;
10
+ --panel: rgba(255, 251, 244, 0.76);
11
+ --panel-strong: rgba(255, 250, 242, 0.92);
12
+ --text: #17212b;
13
+ --muted: #667281;
14
+ --line: rgba(20, 29, 36, 0.09);
15
+ --line-strong: rgba(20, 29, 36, 0.16);
16
+ --accent: #cf6a2c;
17
+ --accent-strong: #9b4717;
18
+ --accent-soft: rgba(207, 106, 44, 0.16);
19
+ --shadow: 0 24px 80px rgba(49, 34, 17, 0.14);
20
+ --shadow-card: 0 18px 40px rgba(33, 37, 41, 0.08);
21
+ }
22
+ * { box-sizing: border-box; }
23
+ html, body { margin: 0; min-height: 100%; }
24
+ body {
25
+ font-family: "Avenir Next", "SF Pro Display", "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
26
+ color: var(--text);
27
+ background:
28
+ radial-gradient(circle at 12% 12%, rgba(207, 106, 44, 0.22), transparent 0 26%),
29
+ radial-gradient(circle at 88% 18%, rgba(69, 119, 104, 0.18), transparent 0 24%),
30
+ radial-gradient(circle at 50% 120%, rgba(33, 58, 78, 0.08), transparent 0 28%),
31
+ linear-gradient(180deg, #f8f4ec 0%, var(--bg) 48%, var(--bg-deep) 100%);
32
+ background-attachment: fixed;
33
+ }
34
+ body::before {
35
+ content: "";
36
+ position: fixed;
37
+ inset: 0;
38
+ background:
39
+ linear-gradient(rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.1)),
40
+ repeating-linear-gradient(
41
+ 90deg,
42
+ transparent 0,
43
+ transparent 31px,
44
+ rgba(255, 255, 255, 0.18) 31px,
45
+ rgba(255, 255, 255, 0.18) 32px
46
+ );
47
+ opacity: 0.22;
48
+ pointer-events: none;
49
+ }
50
+ .page {
51
+ position: relative;
52
+ width: min(1520px, calc(100vw - 32px));
53
+ margin: 0 auto;
54
+ padding: clamp(24px, 3vw, 44px) 0 72px;
55
+ }
56
+ .hero {
57
+ position: relative;
58
+ overflow: hidden;
59
+ isolation: isolate;
60
+ display: grid;
61
+ align-items: end;
62
+ min-height: clamp(240px, 32vw, 380px);
63
+ padding: clamp(28px, 4vw, 52px);
64
+ border-radius: 36px;
65
+ background:
66
+ linear-gradient(135deg, rgba(255, 250, 242, 0.84), rgba(247, 236, 220, 0.88)),
67
+ linear-gradient(180deg, rgba(255, 255, 255, 0.24), rgba(255, 255, 255, 0));
68
+ border: 1px solid rgba(255, 255, 255, 0.68);
69
+ box-shadow: var(--shadow);
70
+ backdrop-filter: blur(20px);
71
+ }
72
+ .hero::before,
73
+ .hero::after {
74
+ content: "";
75
+ position: absolute;
76
+ border-radius: 999px;
77
+ pointer-events: none;
78
+ }
79
+ .hero::before {
80
+ inset: -18% auto auto -8%;
81
+ width: min(34vw, 420px);
82
+ aspect-ratio: 1;
83
+ background: radial-gradient(circle, rgba(244, 161, 85, 0.28), transparent 62%);
84
+ filter: blur(8px);
85
+ opacity: 0.95;
86
+ }
87
+ .hero::after {
88
+ inset: auto -8% -44% auto;
89
+ width: min(42vw, 560px);
90
+ aspect-ratio: 1;
91
+ background: radial-gradient(circle, rgba(49, 83, 104, 0.22), transparent 62%);
92
+ filter: blur(16px);
93
+ }
94
+ .hero-kicker {
95
+ position: relative;
96
+ z-index: 1;
97
+ display: inline-flex;
98
+ width: fit-content;
99
+ margin: 0 0 16px;
100
+ padding: 10px 14px;
101
+ border-radius: 999px;
102
+ border: 1px solid rgba(155, 71, 23, 0.14);
103
+ background: rgba(255, 255, 255, 0.62);
104
+ box-shadow: 0 8px 22px rgba(155, 71, 23, 0.08);
105
+ font-size: clamp(12px, 1vw, 14px);
106
+ letter-spacing: 0.18em;
107
+ text-transform: uppercase;
108
+ color: var(--accent-strong);
109
+ }
110
+ .hero-title {
111
+ position: relative;
112
+ z-index: 1;
113
+ margin: 0;
114
+ max-width: 9ch;
115
+ font-size: clamp(42px, 8vw, 96px);
116
+ line-height: 0.88;
117
+ letter-spacing: -0.06em;
118
+ text-wrap: balance;
119
+ }
120
+ .section-head {
121
+ display: flex;
122
+ align-items: end;
123
+ justify-content: space-between;
124
+ gap: 20px;
125
+ margin: 34px 0 22px;
126
+ padding: 0 6px;
127
+ }
128
+ .section-title {
129
+ margin: 0;
130
+ font-size: clamp(28px, 3vw, 40px);
131
+ line-height: 1;
132
+ letter-spacing: -0.04em;
133
+ }
134
+ .section-note {
135
+ margin: 0;
136
+ font-size: clamp(14px, 1.3vw, 18px);
137
+ color: var(--muted);
138
+ }
139
+ .app-grid {
140
+ display: grid;
141
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
142
+ gap: 20px;
143
+ }
144
+ .app-card {
145
+ position: relative;
146
+ display: flex;
147
+ flex-direction: column;
148
+ justify-content: space-between;
149
+ gap: 24px;
150
+ min-height: 240px;
151
+ padding: 24px;
152
+ border-radius: 30px;
153
+ text-decoration: none;
154
+ color: inherit;
155
+ background:
156
+ linear-gradient(180deg, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.14)),
157
+ var(--panel);
158
+ border: 1px solid var(--line);
159
+ box-shadow: var(--shadow-card);
160
+ backdrop-filter: blur(18px);
161
+ transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease, background 180ms ease;
162
+ }
163
+ .app-card::before,
164
+ .app-card::after {
165
+ content: "";
166
+ position: absolute;
167
+ pointer-events: none;
168
+ }
169
+ .app-card::before {
170
+ inset: 0;
171
+ border-radius: inherit;
172
+ padding: 1px;
173
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0), rgba(207, 106, 44, 0.12));
174
+ -webkit-mask:
175
+ linear-gradient(#000 0 0) content-box,
176
+ linear-gradient(#000 0 0);
177
+ -webkit-mask-composite: xor;
178
+ mask-composite: exclude;
179
+ opacity: 0.85;
180
+ }
181
+ .app-card::after {
182
+ top: -24%;
183
+ right: -10%;
184
+ width: 56%;
185
+ aspect-ratio: 1;
186
+ border-radius: 999px;
187
+ background: radial-gradient(circle, rgba(207, 106, 44, 0.16), transparent 68%);
188
+ transition: transform 180ms ease, opacity 180ms ease;
189
+ }
190
+ .app-card:hover {
191
+ transform: translateY(-6px);
192
+ border-color: rgba(207, 106, 44, 0.3);
193
+ box-shadow: 0 28px 60px rgba(30, 37, 44, 0.14);
194
+ background:
195
+ linear-gradient(180deg, rgba(255, 255, 255, 0.62), rgba(255, 255, 255, 0.18)),
196
+ rgba(255, 251, 244, 0.92);
197
+ }
198
+ .app-card:hover::after {
199
+ transform: scale(1.08);
200
+ opacity: 1;
201
+ }
202
+ .app-card:nth-child(3n + 2)::after {
203
+ background: radial-gradient(circle, rgba(72, 122, 107, 0.18), transparent 68%);
204
+ }
205
+ .app-card:nth-child(3n + 3)::after {
206
+ background: radial-gradient(circle, rgba(53, 92, 128, 0.16), transparent 68%);
207
+ }
208
+ .app-card__top,
209
+ .app-card__bottom {
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: space-between;
213
+ gap: 16px;
214
+ }
215
+ .app-card__top {
216
+ align-items: start;
217
+ }
218
+ .app-eyebrow {
219
+ display: inline-flex;
220
+ margin: 0 0 14px;
221
+ padding: 8px 12px;
222
+ border-radius: 999px;
223
+ background: rgba(255, 255, 255, 0.58);
224
+ border: 1px solid rgba(155, 71, 23, 0.1);
225
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
226
+ font-size: 11px;
227
+ letter-spacing: 0.16em;
228
+ text-transform: uppercase;
229
+ color: var(--accent-strong);
230
+ }
231
+ .app-title {
232
+ margin: 0;
233
+ max-width: 11ch;
234
+ font-size: clamp(28px, 2.8vw, 40px);
235
+ line-height: 0.95;
236
+ letter-spacing: -0.05em;
237
+ overflow-wrap: anywhere;
238
+ }
239
+ .app-link {
240
+ display: block;
241
+ max-width: 100%;
242
+ font-family: "SF Mono", "ui-monospace", "Menlo", monospace;
243
+ font-size: 13px;
244
+ letter-spacing: 0.02em;
245
+ color: var(--muted);
246
+ overflow-wrap: anywhere;
247
+ }
248
+ .app-action {
249
+ flex-shrink: 0;
250
+ padding: 12px 18px;
251
+ border-radius: 999px;
252
+ background: #1d2d38;
253
+ color: #fff;
254
+ border: 1px solid rgba(255, 255, 255, 0.14);
255
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
256
+ font-size: 14px;
257
+ letter-spacing: 0.08em;
258
+ text-transform: uppercase;
259
+ transition: transform 180ms ease, background 180ms ease;
260
+ }
261
+ .app-card:hover .app-action {
262
+ transform: translateX(2px);
263
+ background: #223746;
264
+ }
265
+ .empty-state {
266
+ padding: 40px 32px;
267
+ border-radius: 30px;
268
+ background:
269
+ linear-gradient(180deg, rgba(255, 255, 255, 0.45), rgba(255, 255, 255, 0.12)),
270
+ var(--panel-strong);
271
+ border: 1px dashed var(--line-strong);
272
+ box-shadow: var(--shadow-card);
273
+ }
274
+ .empty-state__title {
275
+ margin: 0;
276
+ font-size: 24px;
277
+ letter-spacing: -0.03em;
278
+ }
279
+ .empty-state__desc {
280
+ margin: 10px 0 0;
281
+ color: var(--muted);
282
+ font-size: 16px;
283
+ line-height: 1.7;
284
+ }
285
+ @media (max-width: 820px) {
286
+ body::before {
287
+ opacity: 0.12;
288
+ }
289
+ .page {
290
+ width: min(100vw - 20px, 100%);
291
+ padding-top: 18px;
292
+ }
293
+ .hero {
294
+ min-height: 220px;
295
+ border-radius: 26px;
296
+ padding: 22px;
9
297
  }
10
- if (app.capabilities.httpApi) {
11
- tags.push('接口');
298
+ .section-head {
299
+ align-items: start;
300
+ flex-direction: column;
301
+ margin-top: 28px;
12
302
  }
13
- if (app.capabilities.event) {
14
- tags.push('事件');
303
+ .app-card {
304
+ min-height: 0;
305
+ padding: 20px;
306
+ border-radius: 24px;
15
307
  }
16
- if (app.capabilities.expose) {
17
- tags.push('Expose');
308
+ .app-card__top,
309
+ .app-card__bottom {
310
+ align-items: start;
311
+ flex-direction: column;
18
312
  }
19
- return tags;
313
+ .app-title {
314
+ max-width: none;
315
+ }
316
+ }
317
+ `;
318
+ const appHref = (app) => {
319
+ return app.kind === 'main' ? '/app/' : `/apps/${app.name}/`;
20
320
  };
21
- const renderCard = (app) => {
321
+ const renderCardHtml = (app) => {
22
322
  const href = appHref(app);
23
- const tags = appTags(app)
24
- .map(tag => `<span class="app-tag">${escapeHtml(tag)}</span>`)
25
- .join('');
26
- const desc = app.kind === 'main' ? '访问主应用页面、接口与默认资源。' : '访问插件页面、接口与公开入口。';
27
323
  return `
28
324
  <a
29
325
  class="app-card"
@@ -37,13 +333,7 @@ const renderCard = (app) => {
37
333
  <p class="app-eyebrow">${app.kind === 'main' ? 'Main App' : 'Plugin App'}</p>
38
334
  <h2 class="app-title">${escapeHtml(app.name)}</h2>
39
335
  </div>
40
- <div class="app-rank">
41
- <span class="app-rank__label">热度</span>
42
- <span class="app-rank__value" data-click-count>0</span>
43
- </div>
44
336
  </div>
45
- <p class="app-desc">${escapeHtml(desc)}</p>
46
- <div class="app-tags">${tags}</div>
47
337
  <div class="app-card__bottom">
48
338
  <span class="app-link">${escapeHtml(href)}</span>
49
339
  <span class="app-action">进入</span>
@@ -51,325 +341,100 @@ const renderCard = (app) => {
51
341
  </a>
52
342
  `;
53
343
  };
54
- const renderEmpty = () => {
344
+ const renderEmptyHtml = () => {
55
345
  return `
56
346
  <div class="empty-state">
57
347
  <p class="empty-state__title">当前没有可展示的应用。</p>
58
- <p class="empty-state__desc">请先加载主应用或插件,再刷新本页查看入口。</p>
348
+ <p class="empty-state__desc">可能你并启动扩展,或选择的扩展并没有支持WEB应用</p>
59
349
  </div>
60
350
  `;
61
351
  };
62
- const renderHelloHtml = (apps) => {
63
- const visibleApps = apps
64
- .filter(app => app.enabled && app.status === 'ready' && (app.capabilities.web || app.capabilities.httpApi))
65
- .sort((left, right) => {
66
- if (left.kind === 'main' && right.kind !== 'main') {
67
- return -1;
68
- }
69
- if (left.kind !== 'main' && right.kind === 'main') {
70
- return 1;
71
- }
72
- return left.name.localeCompare(right.name);
73
- });
74
- const cards = visibleApps.length ? visibleApps.map(renderCard).join('') : renderEmpty();
75
- const payload = JSON.stringify(visibleApps.map(app => ({
352
+ const renderLaunchpadScript = (apps) => {
353
+ const payload = JSON.stringify(apps.map(app => ({
76
354
  id: app.name,
77
355
  href: appHref(app),
78
356
  kind: app.kind
79
357
  })));
80
- return `<!DOCTYPE html>
81
- <html lang="zh-CN">
82
- <head>
83
- <meta charset="utf-8" />
84
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
85
- <title>ALemonJS 应用入口</title>
86
- <style>
87
- :root {
88
- --bg: #f3efe5;
89
- --panel: rgba(255, 251, 245, 0.88);
90
- --panel-strong: #fffaf2;
91
- --text: #17212b;
92
- --muted: #6a7684;
93
- --line: rgba(23, 33, 43, 0.08);
94
- --accent: #d96c28;
95
- --accent-strong: #a54a13;
96
- --shadow: 0 24px 60px rgba(47, 35, 20, 0.12);
97
- }
98
- * { box-sizing: border-box; }
99
- html, body { margin: 0; min-height: 100%; }
100
- body {
101
- font-family: "SF Pro Display", "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
102
- color: var(--text);
103
- background:
104
- radial-gradient(circle at top left, rgba(217, 108, 40, 0.18), transparent 28%),
105
- radial-gradient(circle at right center, rgba(78, 140, 124, 0.14), transparent 26%),
106
- linear-gradient(180deg, #f8f4ec 0%, var(--bg) 100%);
107
- }
108
- .page {
109
- width: min(1680px, calc(100vw - 32px));
110
- margin: 0 auto;
111
- padding: clamp(28px, 4vw, 56px) 0 64px;
112
- }
113
- .hero {
114
- position: relative;
115
- overflow: hidden;
116
- padding: clamp(28px, 4vw, 56px);
117
- border-radius: 32px;
118
- background: linear-gradient(135deg, rgba(255, 250, 242, 0.95), rgba(255, 244, 231, 0.88));
119
- border: 1px solid rgba(255, 255, 255, 0.72);
120
- box-shadow: var(--shadow);
121
- }
122
- .hero::after {
123
- content: "";
124
- position: absolute;
125
- inset: auto -8% -48% auto;
126
- width: min(38vw, 520px);
127
- aspect-ratio: 1;
128
- border-radius: 999px;
129
- background: radial-gradient(circle, rgba(217, 108, 40, 0.18), transparent 62%);
130
- pointer-events: none;
131
- }
132
- .hero-kicker {
133
- margin: 0 0 12px;
134
- font-size: clamp(14px, 1.2vw, 18px);
135
- letter-spacing: 0.18em;
136
- text-transform: uppercase;
137
- color: var(--accent-strong);
138
- }
139
- .hero-title {
140
- margin: 0;
141
- max-width: 12ch;
142
- font-size: clamp(40px, 7vw, 86px);
143
- line-height: 0.95;
144
- letter-spacing: -0.04em;
145
- }
146
- .hero-desc {
147
- margin: 18px 0 0;
148
- max-width: 56rem;
149
- font-size: clamp(18px, 2vw, 28px);
150
- line-height: 1.6;
151
- color: var(--muted);
152
- }
153
- .hero-meta {
154
- display: flex;
155
- flex-wrap: wrap;
156
- gap: 14px;
157
- margin-top: 28px;
158
- }
159
- .hero-pill {
160
- display: inline-flex;
161
- align-items: center;
162
- gap: 10px;
163
- padding: 12px 18px;
164
- border-radius: 999px;
165
- background: rgba(255, 255, 255, 0.78);
166
- border: 1px solid rgba(23, 33, 43, 0.08);
167
- font-size: clamp(14px, 1.4vw, 18px);
168
- }
169
- .hero-pill strong {
170
- color: var(--accent-strong);
171
- }
172
- .section-head {
173
- display: flex;
174
- align-items: end;
175
- justify-content: space-between;
176
- gap: 20px;
177
- margin: 28px 0 18px;
178
- padding: 0 6px;
179
- }
180
- .section-title {
181
- margin: 0;
182
- font-size: clamp(28px, 3vw, 42px);
183
- }
184
- .section-note {
185
- margin: 0;
186
- font-size: clamp(14px, 1.3vw, 18px);
187
- color: var(--muted);
188
- }
189
- .app-grid {
190
- display: grid;
191
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
192
- gap: 18px;
193
- }
194
- .app-card {
195
- display: flex;
196
- flex-direction: column;
197
- gap: 18px;
198
- min-height: 280px;
199
- padding: 24px;
200
- border-radius: 28px;
201
- text-decoration: none;
202
- color: inherit;
203
- background: var(--panel);
204
- border: 1px solid var(--line);
205
- box-shadow: 0 14px 40px rgba(30, 37, 44, 0.08);
206
- transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
207
- }
208
- .app-card:hover {
209
- transform: translateY(-4px);
210
- border-color: rgba(217, 108, 40, 0.28);
211
- box-shadow: 0 24px 50px rgba(30, 37, 44, 0.12);
212
- }
213
- .app-card__top,
214
- .app-card__bottom {
215
- display: flex;
216
- align-items: center;
217
- justify-content: space-between;
218
- gap: 16px;
219
- }
220
- .app-eyebrow {
221
- margin: 0 0 8px;
222
- font-size: 13px;
223
- letter-spacing: 0.16em;
224
- text-transform: uppercase;
225
- color: var(--accent-strong);
226
- }
227
- .app-title {
228
- margin: 0;
229
- font-size: clamp(28px, 2.8vw, 38px);
230
- line-height: 1;
231
- }
232
- .app-desc {
233
- margin: 0;
234
- font-size: clamp(16px, 1.4vw, 20px);
235
- line-height: 1.7;
236
- color: var(--muted);
237
- }
238
- .app-tags {
239
- display: flex;
240
- flex-wrap: wrap;
241
- gap: 10px;
242
- }
243
- .app-tag {
244
- padding: 8px 12px;
245
- border-radius: 999px;
246
- background: rgba(217, 108, 40, 0.1);
247
- color: var(--accent-strong);
248
- font-size: 14px;
249
- }
250
- .app-rank {
251
- min-width: 84px;
252
- text-align: right;
253
- }
254
- .app-rank__label,
255
- .app-link {
256
- display: block;
257
- font-size: 13px;
258
- color: var(--muted);
259
- }
260
- .app-rank__value {
261
- font-size: clamp(24px, 2vw, 30px);
262
- font-weight: 700;
263
- }
264
- .app-action {
265
- padding: 12px 18px;
266
- border-radius: 999px;
267
- background: #1d2d38;
268
- color: #fff;
269
- font-size: 15px;
270
- }
271
- .empty-state {
272
- padding: 32px;
273
- border-radius: 28px;
274
- background: var(--panel-strong);
275
- border: 1px dashed rgba(23, 33, 43, 0.16);
276
- }
277
- .empty-state__title {
278
- margin: 0;
279
- font-size: 24px;
280
- }
281
- .empty-state__desc {
282
- margin: 10px 0 0;
283
- color: var(--muted);
284
- font-size: 16px;
285
- }
286
- @media (max-width: 820px) {
287
- .page { width: min(100vw - 20px, 100%); padding-top: 18px; }
288
- .hero { border-radius: 24px; padding: 22px; }
289
- .section-head { align-items: start; flex-direction: column; }
290
- .app-card { min-height: 0; padding: 20px; border-radius: 22px; }
291
- .app-card__top,
292
- .app-card__bottom { align-items: start; flex-direction: column; }
293
- .app-rank { text-align: left; min-width: 0; }
294
- }
295
- </style>
296
- </head>
297
- <body>
298
- <main class="page">
299
- <section class="hero">
300
- <p class="hero-kicker">ALemonJS Launchpad</p>
301
- <h1 class="hero-title">阿柠檬机器人</h1>
302
- </section>
303
- <section>
304
- <div class="section-head">
305
- <div>
306
- <h2 class="section-title">应用列表</h2>
307
- </div>
308
- </div>
309
- <div class="app-grid" id="app-grid">${cards}</div>
310
- </section>
311
- </main>
312
- <script>
313
- (() => {
314
- const storageKey = 'alemonjs:launchpad:clicks';
315
- const apps = ${payload};
316
- const grid = document.getElementById('app-grid');
317
- if (!grid) return;
358
+ return `
359
+ (() => {
360
+ const storageKey = 'alemonjs:launchpad:clicks';
361
+ const apps = ${payload};
362
+ const grid = document.getElementById('app-grid');
363
+ if (!grid) return;
318
364
 
319
- const readClicks = () => {
320
- try {
321
- return JSON.parse(localStorage.getItem(storageKey) || '{}');
322
- } catch {
323
- return {};
324
- }
325
- };
365
+ const readClicks = () => {
366
+ try {
367
+ return JSON.parse(localStorage.getItem(storageKey) || '{}');
368
+ } catch {
369
+ return {};
370
+ }
371
+ };
326
372
 
327
- const writeClicks = (value) => {
328
- localStorage.setItem(storageKey, JSON.stringify(value));
329
- };
373
+ const writeClicks = (value) => {
374
+ localStorage.setItem(storageKey, JSON.stringify(value));
375
+ };
330
376
 
331
- const scoreOf = (id, clicks) => Number(clicks[id] || 0);
332
- const clicks = readClicks();
333
- const cards = Array.from(grid.querySelectorAll('[data-app-id]'));
377
+ const scoreOf = (id, clicks) => Number(clicks[id] || 0);
378
+ const clicks = readClicks();
379
+ const cards = Array.from(grid.querySelectorAll('[data-app-id]'));
334
380
 
335
- const refreshCounts = () => {
336
- cards.forEach((card) => {
337
- const id = card.getAttribute('data-app-id') || '';
338
- const node = card.querySelector('[data-click-count]');
339
- if (node) {
340
- node.textContent = String(scoreOf(id, clicks));
341
- }
342
- });
343
- };
381
+ const refreshCounts = () => {
382
+ cards.forEach((card) => {
383
+ const id = card.getAttribute('data-app-id') || '';
384
+ const node = card.querySelector('[data-click-count]');
385
+ if (node) {
386
+ node.textContent = String(scoreOf(id, clicks));
387
+ }
388
+ });
389
+ };
344
390
 
345
- const sortCards = () => {
346
- cards
347
- .sort((left, right) => {
348
- const leftId = left.getAttribute('data-app-id') || '';
349
- const rightId = right.getAttribute('data-app-id') || '';
350
- const diff = scoreOf(rightId, clicks) - scoreOf(leftId, clicks);
351
- if (diff !== 0) return diff;
352
- return leftId.localeCompare(rightId);
353
- })
354
- .forEach(card => grid.appendChild(card));
355
- };
391
+ const sortCards = () => {
392
+ cards
393
+ .sort((left, right) => {
394
+ const leftId = left.getAttribute('data-app-id') || '';
395
+ const rightId = right.getAttribute('data-app-id') || '';
396
+ const diff = scoreOf(rightId, clicks) - scoreOf(leftId, clicks);
397
+ if (diff !== 0) return diff;
398
+ return leftId.localeCompare(rightId);
399
+ })
400
+ .forEach(card => grid.appendChild(card));
401
+ };
356
402
 
357
- cards.forEach((card) => {
358
- card.addEventListener('click', () => {
359
- const id = card.getAttribute('data-app-id') || '';
360
- clicks[id] = scoreOf(id, clicks) + 1;
361
- writeClicks(clicks);
362
- refreshCounts();
363
- sortCards();
364
- });
403
+ cards.forEach((card) => {
404
+ card.addEventListener('click', () => {
405
+ const id = card.getAttribute('data-app-id') || '';
406
+ clicks[id] = scoreOf(id, clicks) + 1;
407
+ writeClicks(clicks);
408
+ refreshCounts();
409
+ sortCards();
365
410
  });
411
+ });
366
412
 
367
- refreshCounts();
368
- sortCards();
369
- })();
370
- </script>
371
- </body>
372
- </html>`;
413
+ refreshCounts();
414
+ sortCards();
415
+ })();
416
+ `;
417
+ };
418
+ class LaunchpadPage extends Component {
419
+ render() {
420
+ const visibleApps = this.props.apps
421
+ .filter(app => app.enabled && app.status === 'ready' && (app.capabilities.web || app.capabilities.httpApi))
422
+ .sort((left, right) => {
423
+ if (left.kind === 'main' && right.kind !== 'main') {
424
+ return -1;
425
+ }
426
+ if (left.kind !== 'main' && right.kind === 'main') {
427
+ return 1;
428
+ }
429
+ return left.name.localeCompare(right.name);
430
+ });
431
+ const cards = visibleApps.length ? visibleApps.map(renderCardHtml).join('') : renderEmptyHtml();
432
+ const script = renderLaunchpadScript(visibleApps);
433
+ return Html({ lang: 'zh-CN' }, Head(createElement('meta', { charset: 'utf-8' }), createElement('meta', { name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover' }), Title('ALemonJS 应用入口'), Style(styles)), Body(createElement('main', { className: 'page' }, createElement('section', { className: 'hero' }, P({ className: 'hero-kicker' }, 'ALemonJS Launchpad'), createElement('h1', { className: 'hero-title' }, '阿柠檬机器人')), createElement('section', null, Div({ className: 'section-head' }, Div(null, createElement('h2', { className: 'section-title' }, '应用列表'), P({ className: 'section-note' }, '点击次数越高,卡片排序越靠前。'))), Div({ className: 'app-grid', id: 'app-grid', dangerouslySetInnerHTML: { __html: cards } }))), createElement('script', { dangerouslySetInnerHTML: { __html: script } })));
434
+ }
435
+ }
436
+ const renderHelloHtml = (apps) => {
437
+ return DOCTYPE + renderToString(createElement(LaunchpadPage, { apps }));
373
438
  };
374
439
  var hello_html = renderHelloHtml([]);
375
440
 
@@ -65,6 +65,17 @@ const matchBasePath = (requestPath, basePath) => {
65
65
  }
66
66
  return '';
67
67
  };
68
+ const matchApiBasePath = (requestPath, basePath) => {
69
+ return requestPath === basePath || requestPath.startsWith(`${basePath}/`);
70
+ };
71
+ const needsTrailingSlashRedirect = (requestUrl, basePath) => {
72
+ return requestUrl === basePath || requestUrl.startsWith(`${basePath}?`);
73
+ };
74
+ const getOriginalPathname = (ctx) => {
75
+ const rawUrl = typeof ctx.originalUrl === 'string' && ctx.originalUrl ? ctx.originalUrl : ctx.url;
76
+ const [pathname] = String(rawUrl).split('?');
77
+ return pathname || '/';
78
+ };
68
79
  const rewriteCtxPath = async (ctx, nextPath, handler) => {
69
80
  const search = ctx.querystring ? `?${ctx.querystring}` : '';
70
81
  const originalUrl = ctx.url;
@@ -139,6 +150,99 @@ const denyRuntimeAppAccess = (ctx, appName, capability) => {
139
150
  }
140
151
  return runtimeApp;
141
152
  };
153
+ const getRuntimeAppRouters = (appName) => {
154
+ const registerRouters = getChildrenApp(appName)?.register?.koaRouter;
155
+ const storedRouters = getRuntimeAppKoaRouters(appName);
156
+ return (storedRouters.length ? storedRouters : Array.isArray(registerRouters) ? registerRouters : registerRouters ? [registerRouters] : []).filter(Boolean);
157
+ };
158
+ const dispatchAppKoaRouters = async (ctx, appName) => {
159
+ const runtimeApp = getRuntimeApp(appName);
160
+ if (!runtimeApp || !runtimeApp.enabled || runtimeApp.status !== 'ready' || !hasRuntimeAppCapability(appName, 'httpApi')) {
161
+ return false;
162
+ }
163
+ const routers = getRuntimeAppRouters(appName);
164
+ const aliasBases = appName === 'main' ? ['/app'] : [`/apps/${appName}`];
165
+ for (const koaRouter of routers) {
166
+ for (const basePath of aliasBases) {
167
+ const rewrittenPath = matchBasePath(ctx.path, basePath);
168
+ if (!rewrittenPath) {
169
+ continue;
170
+ }
171
+ try {
172
+ const matchedContext = ctx;
173
+ const beforeMatched = Array.isArray(matchedContext.matched) ? matchedContext.matched.length : 0;
174
+ const beforeStatus = ctx.status;
175
+ const beforeBody = ctx.body;
176
+ const beforeMatchedRoute = ctx._matchedRoute;
177
+ const beforeRouterPath = ctx.routerPath;
178
+ let fallthrough = false;
179
+ await rewriteCtxPath(ctx, rewrittenPath, async () => {
180
+ await koaRouter.routes()(ctx, () => {
181
+ fallthrough = true;
182
+ });
183
+ });
184
+ const afterMatched = Array.isArray(matchedContext.matched) ? matchedContext.matched.length : 0;
185
+ const afterMatchedRoute = ctx._matchedRoute;
186
+ const afterRouterPath = ctx.routerPath;
187
+ const handled = afterMatched > beforeMatched
188
+ || afterMatchedRoute !== beforeMatchedRoute
189
+ || afterRouterPath !== beforeRouterPath
190
+ || ctx.status !== beforeStatus
191
+ || ctx.body !== beforeBody
192
+ || !fallthrough;
193
+ if (!handled) {
194
+ continue;
195
+ }
196
+ await rewriteCtxPath(ctx, rewrittenPath, async () => {
197
+ await koaRouter.allowedMethods()(ctx, async () => { });
198
+ });
199
+ return true;
200
+ }
201
+ catch (error) {
202
+ const handled = await dispatchHttpError({
203
+ ctx,
204
+ error,
205
+ appName,
206
+ path: ctx.path,
207
+ method: ctx.method,
208
+ kind: 'koa-router'
209
+ });
210
+ if (handled) {
211
+ return true;
212
+ }
213
+ logger.warn({
214
+ code: ResultCode.Fail,
215
+ message: `Error request ${ctx.path}:`,
216
+ data: error instanceof Error ? error.message : String(error)
217
+ });
218
+ ctx.status = 500;
219
+ ctx.body = {
220
+ code: 500,
221
+ message: '处理 Koa Router 请求时发生错误。',
222
+ error: error instanceof Error ? error.message : String(error)
223
+ };
224
+ return true;
225
+ }
226
+ }
227
+ }
228
+ return false;
229
+ };
230
+ const isNamespacedHtmlRequest = (ctx) => {
231
+ if (ctx.method !== 'GET') {
232
+ return false;
233
+ }
234
+ if (ctx.path === '/app' || ctx.path.startsWith('/app/')) {
235
+ return !matchApiBasePath(ctx.path, '/app/api');
236
+ }
237
+ if (!ctx.path.startsWith('/apps/')) {
238
+ return false;
239
+ }
240
+ const [, , appName] = ctx.path.split('/');
241
+ if (!appName) {
242
+ return false;
243
+ }
244
+ return !matchApiBasePath(ctx.path, `/apps/${appName}/api`);
245
+ };
142
246
  const dispatchRegisteredKoaRouters = async (ctx) => {
143
247
  const registeredRouters = listRuntimeAppKoaRouters();
144
248
  const candidates = new Set(registeredRouters.map(item => item.name));
@@ -149,80 +253,82 @@ const dispatchRegisteredKoaRouters = async (ctx) => {
149
253
  }
150
254
  });
151
255
  for (const appName of candidates) {
152
- const runtimeApp = getRuntimeApp(appName);
153
- if (!runtimeApp || !runtimeApp.enabled || runtimeApp.status !== 'ready' || !hasRuntimeAppCapability(appName, 'httpApi')) {
154
- continue;
155
- }
156
- const registerRouters = getChildrenApp(appName)?.register?.koaRouter;
157
- const storedRouters = getRuntimeAppKoaRouters(appName);
158
- const routers = (storedRouters.length ? storedRouters : Array.isArray(registerRouters) ? registerRouters : registerRouters ? [registerRouters] : []).filter(Boolean);
159
- const aliasBases = appName === 'main' ? ['', '/app'] : ['', `/apps/${appName}`];
160
- for (const koaRouter of routers) {
161
- for (const basePath of aliasBases) {
162
- const rewrittenPath = matchBasePath(ctx.path, basePath);
163
- if (!rewrittenPath) {
164
- continue;
165
- }
166
- try {
167
- const matchedContext = ctx;
168
- const beforeMatched = Array.isArray(matchedContext.matched) ? matchedContext.matched.length : 0;
169
- const beforeStatus = ctx.status;
170
- const beforeBody = ctx.body;
171
- const beforeMatchedRoute = ctx._matchedRoute;
172
- const beforeRouterPath = ctx.routerPath;
173
- let fallthrough = false;
174
- await rewriteCtxPath(ctx, rewrittenPath, async () => {
175
- await koaRouter.routes()(ctx, async () => {
176
- fallthrough = true;
177
- });
178
- });
179
- const afterMatched = Array.isArray(matchedContext.matched) ? matchedContext.matched.length : 0;
180
- const afterMatchedRoute = ctx._matchedRoute;
181
- const afterRouterPath = ctx.routerPath;
182
- const handled = afterMatched > beforeMatched ||
183
- afterMatchedRoute !== beforeMatchedRoute ||
184
- afterRouterPath !== beforeRouterPath ||
185
- ctx.status !== beforeStatus ||
186
- ctx.body !== beforeBody ||
187
- !fallthrough;
188
- if (!handled) {
189
- continue;
190
- }
191
- await rewriteCtxPath(ctx, rewrittenPath, async () => {
192
- await koaRouter.allowedMethods()(ctx, async () => { });
193
- });
194
- return true;
195
- }
196
- catch (error) {
197
- const handled = await dispatchHttpError({
198
- ctx,
199
- error,
200
- appName,
201
- path: ctx.path,
202
- method: ctx.method,
203
- kind: 'koa-router'
204
- });
205
- if (handled) {
206
- return true;
207
- }
208
- logger.warn({
209
- code: ResultCode.Fail,
210
- message: `Error request ${ctx.path}:`,
211
- data: error instanceof Error ? error.message : String(error)
212
- });
213
- ctx.status = 500;
214
- ctx.body = {
215
- code: 500,
216
- message: '处理 Koa Router 请求时发生错误。',
217
- error: error instanceof Error ? error.message : String(error)
218
- };
219
- return true;
220
- }
221
- }
256
+ const handled = await dispatchAppKoaRouters(ctx, appName);
257
+ if (handled) {
258
+ return true;
222
259
  }
223
260
  }
224
261
  return false;
225
262
  };
263
+ const serveStaticResource = async (ctx, appName, rootDir, resourcePath) => {
264
+ const packageRoot = resolvePackageRoot(rootDir);
265
+ let root = '';
266
+ try {
267
+ root = readWebRootConfig(packageRoot);
268
+ }
269
+ catch (err) {
270
+ const handled = await dispatchHttpError({
271
+ ctx,
272
+ error: err,
273
+ appName,
274
+ path: ctx.path,
275
+ method: ctx.method,
276
+ kind: 'web'
277
+ });
278
+ if (handled) {
279
+ return 'handled';
280
+ }
281
+ ctx.status = 500;
282
+ ctx.body = {
283
+ code: 500,
284
+ message: '加载 package.json 时发生错误。',
285
+ error: err instanceof Error ? err.message : String(err)
286
+ };
287
+ return 'handled';
288
+ }
289
+ const webRoot = root ? path__default.join(packageRoot, root) : packageRoot;
290
+ const fullPath = safePath(webRoot, resourcePath);
291
+ if (!fullPath) {
292
+ ctx.status = 403;
293
+ ctx.body = {
294
+ code: 403,
295
+ message: '非法路径',
296
+ data: null
297
+ };
298
+ return 'handled';
299
+ }
300
+ if (!existsSync(fullPath)) {
301
+ return 'miss';
302
+ }
303
+ try {
304
+ const file = await fs__default.promises.readFile(fullPath);
305
+ const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
306
+ ctx.set('Content-Type', mimeType);
307
+ ctx.body = file;
308
+ ctx.status = 200;
309
+ return 'handled';
310
+ }
311
+ catch (err) {
312
+ const handled = await dispatchHttpError({
313
+ ctx,
314
+ error: err,
315
+ appName,
316
+ path: ctx.path,
317
+ method: ctx.method,
318
+ kind: 'web'
319
+ });
320
+ if (handled) {
321
+ return 'handled';
322
+ }
323
+ ctx.status = 500;
324
+ ctx.body = {
325
+ code: 500,
326
+ message: appName === 'main' ? '加载资源时发生服务器错误。' : `加载子应用 '${appName}' 资源时发生服务器错误。`,
327
+ error: err instanceof Error ? err.message : String(err)
328
+ };
329
+ return 'handled';
330
+ }
331
+ };
226
332
  router.get('/', ctx => {
227
333
  ctx.status = 200;
228
334
  ctx.set('Content-Type', 'text/html; charset=utf-8');
@@ -271,6 +377,10 @@ router.get('api/runtime/apps/:app', ctx => {
271
377
  };
272
378
  });
273
379
  router.use(async (ctx, next) => {
380
+ if (isNamespacedHtmlRequest(ctx)) {
381
+ await next();
382
+ return;
383
+ }
274
384
  const handled = await dispatchRegisteredKoaRouters(ctx);
275
385
  if (handled) {
276
386
  return;
@@ -278,6 +388,10 @@ router.use(async (ctx, next) => {
278
388
  await next();
279
389
  });
280
390
  const handleMainAppRequest = async (ctx) => {
391
+ if (needsTrailingSlashRedirect(getOriginalPathname(ctx), '/app')) {
392
+ ctx.redirect('/app/');
393
+ return;
394
+ }
281
395
  if (!process.env.input) {
282
396
  ctx.status = 400;
283
397
  ctx.body = {
@@ -289,7 +403,7 @@ const handleMainAppRequest = async (ctx) => {
289
403
  }
290
404
  const rootPath = process.cwd();
291
405
  const apiPath = '/app/api';
292
- if (ctx.path.startsWith(apiPath)) {
406
+ if (matchApiBasePath(ctx.path, apiPath)) {
293
407
  const runtimeApp = denyRuntimeAppAccess(ctx, 'main', 'httpApi');
294
408
  if (!runtimeApp) {
295
409
  return;
@@ -370,83 +484,31 @@ const handleMainAppRequest = async (ctx) => {
370
484
  ctx.status = 405;
371
485
  return;
372
486
  }
373
- let root = '';
487
+ const runtimeApp = getRuntimeApp('main');
374
488
  const resourcePath = formatPath(ctx.params?.path);
375
- const runtimeApp = denyRuntimeAppAccess(ctx, 'main', 'web');
376
- if (!runtimeApp) {
489
+ if (!runtimeApp?.enabled || runtimeApp.status !== 'ready') {
490
+ denyRuntimeAppAccess(ctx, 'main', 'web');
377
491
  return;
378
492
  }
379
- const packageRoot = resolvePackageRoot(runtimeApp.rootDir);
380
- try {
381
- root = readWebRootConfig(packageRoot);
382
- }
383
- catch (err) {
384
- const handled = await dispatchHttpError({
385
- ctx,
386
- error: err,
387
- appName: 'main',
388
- path: ctx.path,
389
- method: ctx.method,
390
- kind: 'web'
391
- });
392
- if (handled) {
493
+ if (hasRuntimeAppCapability('main', 'web')) {
494
+ const staticResult = await serveStaticResource(ctx, 'main', runtimeApp.rootDir, resourcePath);
495
+ if (staticResult === 'handled') {
393
496
  return;
394
497
  }
395
- ctx.status = 500;
396
- ctx.body = {
397
- code: 500,
398
- message: '加载 package.json 时发生错误。',
399
- error: err instanceof Error ? err.message : String(err)
400
- };
401
- return;
402
498
  }
403
- const webRoot = root ? path__default.join(packageRoot, root) : packageRoot;
404
- const fullPath = safePath(webRoot, resourcePath);
405
- if (!fullPath) {
406
- ctx.status = 403;
407
- ctx.body = {
408
- code: 403,
409
- message: '非法路径',
410
- data: null
411
- };
499
+ if (await dispatchAppKoaRouters(ctx, 'main')) {
412
500
  return;
413
501
  }
414
- try {
415
- const file = await fs__default.promises.readFile(fullPath);
416
- const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
417
- ctx.set('Content-Type', mimeType);
418
- ctx.body = file;
419
- ctx.status = 200;
420
- }
421
- catch (err) {
422
- const handled = await dispatchHttpError({
423
- ctx,
424
- error: err,
425
- appName: 'main',
426
- path: ctx.path,
427
- method: ctx.method,
428
- kind: 'web'
429
- });
430
- if (handled) {
431
- return;
432
- }
433
- if (err?.status === 404) {
434
- ctx.status = 404;
435
- ctx.body = {
436
- code: 404,
437
- message: '资源中未找到。',
438
- data: null
439
- };
440
- }
441
- else {
442
- ctx.status = 500;
443
- ctx.body = {
444
- code: 500,
445
- message: '加载资源时发生服务器错误。',
446
- error: err instanceof Error ? err.message : String(err)
447
- };
448
- }
502
+ if (!hasRuntimeAppCapability('main', 'web')) {
503
+ denyRuntimeAppAccess(ctx, 'main', 'web');
504
+ return;
449
505
  }
506
+ ctx.status = 404;
507
+ ctx.body = {
508
+ code: 404,
509
+ message: '资源中未找到。',
510
+ data: null
511
+ };
450
512
  };
451
513
  router.all('app', handleMainAppRequest);
452
514
  router.all('app/', handleMainAppRequest);
@@ -462,8 +524,12 @@ const handlePluginAppRequest = async (ctx) => {
462
524
  };
463
525
  return;
464
526
  }
527
+ if (needsTrailingSlashRedirect(getOriginalPathname(ctx), `/apps/${appName}`)) {
528
+ ctx.redirect(`/apps/${appName}/`);
529
+ return;
530
+ }
465
531
  const apiPath = `/apps/${appName}/api`;
466
- if (ctx.path.startsWith(apiPath)) {
532
+ if (matchApiBasePath(ctx.path, apiPath)) {
467
533
  const runtimeApp = denyRuntimeAppAccess(ctx, appName, 'httpApi');
468
534
  if (!runtimeApp) {
469
535
  return;
@@ -538,88 +604,31 @@ const handlePluginAppRequest = async (ctx) => {
538
604
  ctx.status = 405;
539
605
  return;
540
606
  }
541
- const runtimeApp = denyRuntimeAppAccess(ctx, appName, 'web');
542
- if (!runtimeApp) {
543
- return;
544
- }
545
- const packageRoot = resolvePackageRoot(runtimeApp.rootDir);
607
+ const runtimeApp = getRuntimeApp(appName);
546
608
  const resourcePath = formatPath(ctx.params?.path);
547
- let root = '';
548
- try {
549
- root = readWebRootConfig(packageRoot);
609
+ if (!runtimeApp?.enabled || runtimeApp.status !== 'ready') {
610
+ denyRuntimeAppAccess(ctx, appName, 'web');
611
+ return;
550
612
  }
551
- catch (err) {
552
- const handled = await dispatchHttpError({
553
- ctx,
554
- error: err,
555
- appName,
556
- path: ctx.path,
557
- method: ctx.method,
558
- kind: 'web'
559
- });
560
- if (handled) {
613
+ if (hasRuntimeAppCapability(appName, 'web')) {
614
+ const staticResult = await serveStaticResource(ctx, appName, runtimeApp.rootDir, resourcePath);
615
+ if (staticResult === 'handled') {
561
616
  return;
562
617
  }
563
- ctx.status = 500;
564
- ctx.body = {
565
- code: 500,
566
- message: '加载 package.json 时发生错误。',
567
- error: err instanceof Error ? err.message : String(err)
568
- };
569
- return;
570
618
  }
571
- const webRoot = root ? path__default.join(packageRoot, root) : packageRoot;
572
- const fullPath = safePath(webRoot, resourcePath);
573
- if (!fullPath) {
574
- ctx.status = 403;
575
- ctx.body = {
576
- code: 403,
577
- message: '非法路径',
578
- data: null
579
- };
619
+ if (await dispatchAppKoaRouters(ctx, appName)) {
580
620
  return;
581
621
  }
582
- try {
583
- const file = await fs__default.promises.readFile(fullPath);
584
- const mimeType = mime.lookup(fullPath) || 'application/octet-stream';
585
- ctx.set('Content-Type', mimeType);
586
- ctx.body = file;
587
- ctx.status = 200;
588
- }
589
- catch (err) {
590
- const handled = await dispatchHttpError({
591
- ctx,
592
- error: err,
593
- appName,
594
- path: ctx.path,
595
- method: ctx.method,
596
- kind: 'web'
597
- });
598
- if (handled) {
599
- return;
600
- }
601
- if (err?.status === 404) {
602
- ctx.status = 404;
603
- ctx.body = {
604
- code: 404,
605
- message: `资源 '${resourcePath}' 在子应用 '${appName}' 中未找到。`,
606
- data: null
607
- };
608
- }
609
- else {
610
- logger.warn({
611
- code: ResultCode.Fail,
612
- message: `Error request ${ctx.path}:`,
613
- data: err instanceof Error ? err.message : String(err)
614
- });
615
- ctx.status = 500;
616
- ctx.body = {
617
- code: 500,
618
- message: `加载子应用 '${appName}' 资源时发生服务器错误。`,
619
- error: err instanceof Error ? err.message : String(err)
620
- };
621
- }
622
+ if (!hasRuntimeAppCapability(appName, 'web')) {
623
+ denyRuntimeAppAccess(ctx, appName, 'web');
624
+ return;
622
625
  }
626
+ ctx.status = 404;
627
+ ctx.body = {
628
+ code: 404,
629
+ message: `资源 '${resourcePath}' 在子应用 '${appName}' 中未找到。`,
630
+ data: null
631
+ };
623
632
  };
624
633
  router.all('apps/:app', handlePluginAppRequest);
625
634
  router.all('apps/:app/', handlePluginAppRequest);
@@ -35,16 +35,30 @@ const getModuelFile = (dir) => {
35
35
  return '';
36
36
  };
37
37
  const formatPath = (pathValue) => {
38
- if (!pathValue || pathValue === '/') {
38
+ const normalizedPath = String(pathValue || '')
39
+ .replace(/\\/g, '/')
40
+ .replace(/^\/+/, '')
41
+ .replace(/\/{2,}/g, '/')
42
+ .trim();
43
+ if (!normalizedPath) {
39
44
  return 'index.html';
40
45
  }
41
- const pates = pathValue.split('/');
42
- const lastPath = pates[pates.length - 1];
46
+ if (normalizedPath === 'index' || normalizedPath === 'index.html') {
47
+ return 'index.html';
48
+ }
49
+ if (normalizedPath.endsWith('/')) {
50
+ return `${normalizedPath}index.html`;
51
+ }
52
+ const parts = normalizedPath.split('/');
53
+ const lastPath = parts[parts.length - 1];
54
+ if (lastPath === 'index' || lastPath === 'index.html') {
55
+ parts[parts.length - 1] = 'index.html';
56
+ return parts.join('/');
57
+ }
43
58
  if (lastPath.includes('.')) {
44
- return pathValue;
59
+ return normalizedPath;
45
60
  }
46
- pathValue += '.html';
47
- return pathValue;
61
+ return `${normalizedPath}.html`;
48
62
  };
49
63
 
50
64
  export { formatPath, getModuelFile, isValidPackageName, safePath };
@@ -32,7 +32,7 @@ class Component {
32
32
  function attrsToString(props = {}) {
33
33
  const parts = [];
34
34
  for (const key of Object.keys(props)) {
35
- if (key === 'children') {
35
+ if (key === 'children' || key === 'dangerouslySetInnerHTML') {
36
36
  continue;
37
37
  }
38
38
  const val = props[key];
@@ -59,6 +59,16 @@ function attrsToString(props = {}) {
59
59
  return parts.length ? ' ' + parts.join(' ') : '';
60
60
  }
61
61
  const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
62
+ const rawTextTags = new Set(['script', 'style']);
63
+ function renderRawText(vnode) {
64
+ if (vnode === null || vnode === undefined || vnode === false) {
65
+ return '';
66
+ }
67
+ if (typeof vnode === 'string' || typeof vnode === 'number') {
68
+ return String(vnode);
69
+ }
70
+ return renderToString(vnode);
71
+ }
62
72
  function renderToString(vnode) {
63
73
  if (vnode === null || vnode === undefined || vnode === false) {
64
74
  return '';
@@ -92,12 +102,19 @@ function renderToString(vnode) {
92
102
  if (voidTags.has(tag.toLowerCase())) {
93
103
  return `<${tag}${attrs} />`;
94
104
  }
95
- const children = (vnode.children ?? []).map(renderToString).join('');
105
+ const children = rawTextTags.has(tag.toLowerCase())
106
+ ? (vnode.children ?? []).map(renderRawText).join('')
107
+ : (vnode.children ?? []).map(renderToString).join('');
96
108
  return `<${tag}${attrs}>${children}</${tag}>`;
97
109
  }
98
110
  function makeTag(tag) {
99
111
  return function tagFactory(first, ...restChildren) {
100
- const looksLikeProps = first !== null && typeof first === 'object' && !Array.isArray(first) && Object.prototype.toString.call(first) === '[object Object]';
112
+ const looksLikeVNode = first !== null &&
113
+ typeof first === 'object' &&
114
+ !Array.isArray(first) &&
115
+ Object.prototype.toString.call(first) === '[object Object]' &&
116
+ ('type' in first || 'children' in first);
117
+ const looksLikeProps = first !== null && typeof first === 'object' && !Array.isArray(first) && Object.prototype.toString.call(first) === '[object Object]' && !looksLikeVNode;
101
118
  if (looksLikeProps) {
102
119
  return createElement(tag, first, ...restChildren);
103
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alemonjs",
3
- "version": "2.1.84",
3
+ "version": "2.1.86",
4
4
  "description": "bot script",
5
5
  "author": "lemonade",
6
6
  "license": "MIT",