agent-loft 0.2.29 → 0.2.31

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 (69) hide show
  1. package/README.md +5 -0
  2. package/package.json +1 -1
  3. package/src/agents/claude/driver.ts +3 -2
  4. package/src/agents/claude/session.ts +11 -4
  5. package/src/agents/codex/session.ts +9 -3
  6. package/src/channels/feishu/bot.ts +8 -4
  7. package/src/channels/feishu/turn-stream.ts +3 -2
  8. package/src/channels/web/api.ts +4 -0
  9. package/src/channels/web/server.ts +14 -2
  10. package/src/channels/web/tasks-api.ts +94 -0
  11. package/src/core/chat-mode.ts +14 -14
  12. package/src/core/hub.ts +95 -16
  13. package/src/core/memory-mcp.ts +1 -1
  14. package/src/core/personal.ts +3 -1
  15. package/src/core/store.ts +2 -0
  16. package/src/core/task-prompt.ts +38 -0
  17. package/src/core/task-runner.ts +232 -0
  18. package/src/core/task-schedule.ts +250 -0
  19. package/src/core/tasks-bridge.ts +252 -0
  20. package/src/core/tasks-mcp.ts +55 -0
  21. package/src/core/tasks.ts +281 -0
  22. package/src/core/types.ts +2 -0
  23. package/src/main.ts +8 -0
  24. package/src/mcp/tasks-server.ts +206 -0
  25. package/web/app.js +8 -5560
  26. package/web/css/base.css +291 -0
  27. package/web/css/composer.css +506 -0
  28. package/web/css/loader.css +196 -0
  29. package/web/css/messages.css +734 -0
  30. package/web/css/mobile.css +100 -0
  31. package/web/css/overlay.css +437 -0
  32. package/web/css/schedule.css +93 -0
  33. package/web/css/settings.css +335 -0
  34. package/web/css/sidebar.css +593 -0
  35. package/web/css/tasks.css +455 -0
  36. package/web/css/topbar.css +205 -0
  37. package/web/index.html +12 -1
  38. package/web/lib/api.js +26 -0
  39. package/web/lib/dom.js +101 -0
  40. package/web/lib/markdown.js +52 -0
  41. package/web/lib/overlay.js +144 -0
  42. package/web/lib/push.js +70 -0
  43. package/web/lib/time.js +60 -0
  44. package/web/lib/ws.js +236 -0
  45. package/web/settings/agents.js +210 -0
  46. package/web/settings/commands.js +411 -0
  47. package/web/settings/common.js +42 -0
  48. package/web/settings/feishu.js +181 -0
  49. package/web/settings/general.js +184 -0
  50. package/web/settings/index.js +63 -0
  51. package/web/settings/memory.js +366 -0
  52. package/web/settings/notify.js +80 -0
  53. package/web/settings/security.js +188 -0
  54. package/web/settings/users.js +189 -0
  55. package/web/state.js +46 -0
  56. package/web/views/auth.js +61 -0
  57. package/web/views/composer.js +324 -0
  58. package/web/views/conv-actions.js +412 -0
  59. package/web/views/interaction.js +140 -0
  60. package/web/views/layout.js +197 -0
  61. package/web/views/messages.js +531 -0
  62. package/web/views/quota.js +145 -0
  63. package/web/views/schedule.js +120 -0
  64. package/web/views/sidebar.js +458 -0
  65. package/web/views/slash.js +185 -0
  66. package/web/views/tasks.js +460 -0
  67. package/web/views/topbar.js +400 -0
  68. package/web/views/voice.js +189 -0
  69. package/web/style.css +0 -3500
@@ -0,0 +1,455 @@
1
+ /* ---------- 定时任务 ---------- */
2
+
3
+ /* 侧栏「最近」上面的入口:和「新聊天」同一个样子 */
4
+ .side-nav {
5
+ display: flex;
6
+ justify-content: flex-start;
7
+ width: calc(100% - 20px);
8
+ margin: 2px 10px 6px;
9
+ padding: 7px 10px;
10
+ gap: 8px;
11
+ background: transparent;
12
+ border: none;
13
+ font-size: 14px;
14
+ font-weight: 400;
15
+ }
16
+
17
+ .side-nav svg {
18
+ width: 16px;
19
+ height: 16px;
20
+ }
21
+
22
+ .side-nav:hover,
23
+ .side-nav.active {
24
+ background: var(--bg-hover);
25
+ }
26
+
27
+ .conv-item .task-mark {
28
+ color: var(--fg-soft);
29
+ }
30
+
31
+ /* 任务页不要输入框,也不要「回到底部」 */
32
+ .main.tasks-view .composer-wrap,
33
+ .main.tasks-view .to-bottom {
34
+ display: none;
35
+ }
36
+
37
+ .tasks-page {
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 16px;
41
+ padding-top: 8px;
42
+ }
43
+
44
+ .tasks-head {
45
+ display: flex;
46
+ align-items: flex-start;
47
+ gap: 16px;
48
+ }
49
+
50
+ .tasks-head .grow {
51
+ flex: 1;
52
+ min-width: 0;
53
+ }
54
+
55
+ .tasks-head h2 {
56
+ margin: 0 0 6px;
57
+ font-size: 24px;
58
+ font-weight: 600;
59
+ }
60
+
61
+ .tasks-head p {
62
+ margin: 0;
63
+ font-size: 13px;
64
+ line-height: 1.6;
65
+ color: var(--fg-soft);
66
+ }
67
+
68
+ .tasks-head .btn {
69
+ flex-shrink: 0;
70
+ }
71
+
72
+ .task-list {
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: 12px;
76
+ }
77
+
78
+ .task-card {
79
+ border: 1px solid var(--border);
80
+ border-radius: var(--radius);
81
+ padding: 12px 14px 12px 16px;
82
+ background: var(--bg);
83
+ }
84
+
85
+ .task-card.off .task-title,
86
+ .task-card.off .task-meta,
87
+ .task-card.off .task-prompt {
88
+ opacity: 0.6;
89
+ }
90
+
91
+ .task-top {
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 8px;
95
+ }
96
+
97
+ .task-title {
98
+ flex: 1;
99
+ min-width: 0;
100
+ padding: 0;
101
+ text-align: left;
102
+ font-size: 16px;
103
+ font-weight: 600;
104
+ overflow: hidden;
105
+ text-overflow: ellipsis;
106
+ white-space: nowrap;
107
+ }
108
+
109
+ .task-meta {
110
+ display: flex;
111
+ flex-wrap: wrap;
112
+ gap: 2px 0;
113
+ margin-top: 4px;
114
+ font-size: 13px;
115
+ color: var(--fg-soft);
116
+ }
117
+
118
+ /* 分隔点挂在前一项的末尾:换行时点留在行尾,不会出现在下一行开头 */
119
+ .task-meta span:not(:last-child)::after {
120
+ content: "·";
121
+ margin: 0 8px;
122
+ color: var(--fg-faint);
123
+ }
124
+
125
+ .task-prompt {
126
+ margin-top: 8px;
127
+ font-size: 13px;
128
+ line-height: 1.6;
129
+ color: var(--fg-soft);
130
+ white-space: pre-wrap;
131
+ display: -webkit-box;
132
+ -webkit-line-clamp: 2;
133
+ -webkit-box-orient: vertical;
134
+ overflow: hidden;
135
+ }
136
+
137
+ .task-run {
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 8px;
141
+ width: 100%;
142
+ min-width: 0;
143
+ margin-top: 10px;
144
+ padding: 8px 10px;
145
+ border-radius: 10px;
146
+ background: var(--bg-soft);
147
+ font-size: 13px;
148
+ text-align: left;
149
+ }
150
+
151
+ button.task-run:hover {
152
+ background: var(--bg-hover);
153
+ }
154
+
155
+ .task-run.compact {
156
+ margin-top: 2px;
157
+ padding: 6px 10px;
158
+ background: transparent;
159
+ }
160
+
161
+ .task-run.muted {
162
+ color: var(--fg-faint);
163
+ }
164
+
165
+ .task-status {
166
+ display: inline-flex;
167
+ align-items: center;
168
+ gap: 5px;
169
+ flex-shrink: 0;
170
+ padding: 1px 8px;
171
+ border-radius: 999px;
172
+ font-size: 12px;
173
+ border: 1px solid currentColor;
174
+ }
175
+
176
+ .task-status .conv-spin {
177
+ width: 10px;
178
+ height: 10px;
179
+ }
180
+
181
+ .task-status.ok {
182
+ color: var(--ok);
183
+ }
184
+
185
+ .task-status.quiet {
186
+ color: var(--fg-faint);
187
+ }
188
+
189
+ .task-status.bad {
190
+ color: var(--danger);
191
+ }
192
+
193
+ .task-status.run {
194
+ color: var(--accent);
195
+ }
196
+
197
+ .task-run-at {
198
+ flex-shrink: 0;
199
+ color: var(--fg-faint);
200
+ }
201
+
202
+ .task-run-text {
203
+ flex: 1;
204
+ min-width: 0;
205
+ overflow: hidden;
206
+ text-overflow: ellipsis;
207
+ white-space: nowrap;
208
+ color: var(--fg-soft);
209
+ }
210
+
211
+ .task-history summary {
212
+ margin-top: 8px;
213
+ font-size: 12px;
214
+ color: var(--fg-faint);
215
+ cursor: pointer;
216
+ }
217
+
218
+ .task-history summary:hover {
219
+ color: var(--fg-soft);
220
+ }
221
+
222
+ /* 开关 */
223
+ .switch {
224
+ position: relative;
225
+ display: inline-block;
226
+ width: 36px;
227
+ height: 20px;
228
+ flex-shrink: 0;
229
+ cursor: pointer;
230
+ }
231
+
232
+ .switch input {
233
+ position: absolute;
234
+ opacity: 0;
235
+ width: 0;
236
+ height: 0;
237
+ }
238
+
239
+ .switch .slider {
240
+ position: absolute;
241
+ inset: 0;
242
+ border-radius: 999px;
243
+ background: var(--border);
244
+ transition: background 0.15s;
245
+ }
246
+
247
+ .switch .slider::before {
248
+ content: "";
249
+ position: absolute;
250
+ left: 2px;
251
+ top: 2px;
252
+ width: 16px;
253
+ height: 16px;
254
+ border-radius: 50%;
255
+ background: #fff;
256
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
257
+ transition: transform 0.15s;
258
+ }
259
+
260
+ .switch input:checked + .slider {
261
+ background: var(--accent);
262
+ }
263
+
264
+ .switch input:checked + .slider::before {
265
+ transform: translateX(16px);
266
+ }
267
+
268
+ .switch input:focus-visible + .slider {
269
+ outline: 2px solid var(--accent);
270
+ outline-offset: 2px;
271
+ }
272
+
273
+ /* 空状态 + 例子 */
274
+ .tasks-empty {
275
+ display: flex;
276
+ flex-direction: column;
277
+ align-items: center;
278
+ gap: 6px;
279
+ padding: 32px 0 8px;
280
+ text-align: center;
281
+ }
282
+
283
+ .tasks-empty-icon svg {
284
+ width: 40px;
285
+ height: 40px;
286
+ color: var(--fg-faint);
287
+ }
288
+
289
+ .tasks-empty .t {
290
+ font-size: 17px;
291
+ font-weight: 600;
292
+ }
293
+
294
+ .tasks-empty > .s {
295
+ font-size: 13px;
296
+ color: var(--fg-soft);
297
+ margin-bottom: 10px;
298
+ }
299
+
300
+ .task-examples {
301
+ display: grid;
302
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
303
+ gap: 10px;
304
+ width: 100%;
305
+ }
306
+
307
+ .task-example {
308
+ text-align: left;
309
+ padding: 12px 14px;
310
+ border: 1px solid var(--border);
311
+ border-radius: 12px;
312
+ }
313
+
314
+ .task-example:hover {
315
+ background: var(--bg-hover);
316
+ }
317
+
318
+ .task-example .t {
319
+ font-size: 14px;
320
+ font-weight: 600;
321
+ margin-bottom: 4px;
322
+ }
323
+
324
+ .task-example .s {
325
+ font-size: 12px;
326
+ line-height: 1.6;
327
+ color: var(--fg-soft);
328
+ display: -webkit-box;
329
+ -webkit-line-clamp: 3;
330
+ -webkit-box-orient: vertical;
331
+ overflow: hidden;
332
+ }
333
+
334
+ .tasks-tip {
335
+ margin: 4px 0 0;
336
+ font-size: 12px;
337
+ color: var(--fg-faint);
338
+ text-align: center;
339
+ }
340
+
341
+ /* 表单 */
342
+ .task-form [hidden] {
343
+ display: none !important;
344
+ }
345
+
346
+ .task-form textarea {
347
+ resize: vertical;
348
+ font: inherit;
349
+ line-height: 1.6;
350
+ }
351
+
352
+ .task-dedicated {
353
+ display: flex;
354
+ flex-direction: column;
355
+ gap: 12px;
356
+ }
357
+
358
+ /* 频率:上面选种类,下面填具体时间 */
359
+ .task-freq-row {
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: 8px;
363
+ }
364
+
365
+ .task-freq {
366
+ display: flex;
367
+ flex-wrap: wrap;
368
+ align-items: center;
369
+ gap: 8px;
370
+ }
371
+
372
+ .task-freq:empty {
373
+ display: none;
374
+ }
375
+
376
+ .field .task-freq input,
377
+ .field .task-freq select {
378
+ width: auto;
379
+ color-scheme: light dark;
380
+ }
381
+
382
+ .field .task-freq input:not(.narrow):not([type="time"]) {
383
+ flex: 1;
384
+ min-width: 180px;
385
+ }
386
+
387
+ .field .task-freq .narrow {
388
+ width: 84px;
389
+ }
390
+
391
+ .freq-word {
392
+ font-size: 14px;
393
+ color: var(--fg-soft);
394
+ }
395
+
396
+ .day-chips {
397
+ display: flex;
398
+ gap: 4px;
399
+ }
400
+
401
+ .day-chip {
402
+ width: 34px;
403
+ height: 34px;
404
+ border-radius: 50%;
405
+ border: 1px solid var(--border);
406
+ font-size: 13px;
407
+ }
408
+
409
+ .day-chip.on {
410
+ background: var(--accent);
411
+ border-color: var(--accent);
412
+ color: var(--accent-fg);
413
+ }
414
+
415
+ .task-static {
416
+ font-size: 14px;
417
+ color: var(--fg-soft);
418
+ padding: 2px 0;
419
+ }
420
+
421
+ .task-cwd {
422
+ justify-content: flex-start;
423
+ overflow: hidden;
424
+ text-overflow: ellipsis;
425
+ white-space: nowrap;
426
+ }
427
+
428
+ /* 对话里:定时任务这次判断不用提醒,折叠成一行 */
429
+ .silent-run {
430
+ font-size: 13px;
431
+ color: var(--fg-faint);
432
+ }
433
+
434
+ .silent-run summary {
435
+ cursor: pointer;
436
+ list-style: none;
437
+ }
438
+
439
+ .silent-run summary::-webkit-details-marker {
440
+ display: none;
441
+ }
442
+
443
+ .silent-run summary:hover {
444
+ color: var(--fg-soft);
445
+ }
446
+
447
+ .silent-run[open] summary {
448
+ margin-bottom: 8px;
449
+ }
450
+
451
+ @media (max-width: 640px) {
452
+ .tasks-head h2 {
453
+ font-size: 21px;
454
+ }
455
+ }
@@ -0,0 +1,205 @@
1
+ .main {
2
+ flex: 1;
3
+ display: flex;
4
+ flex-direction: column;
5
+ min-width: 0;
6
+ position: relative;
7
+ }
8
+
9
+ .topbar {
10
+ display: flex;
11
+ align-items: center;
12
+ gap: 6px;
13
+ padding: calc(8px + env(safe-area-inset-top)) 12px 8px;
14
+ min-height: 52px;
15
+ /*
16
+ * iOS 26+ 的主屏 PWA 会在页面顶端叠一层系统的模糊渐变(Liquid Glass 边缘效果),CSS 关不掉。
17
+ * WebKit 只在顶端有「fixed / sticky、铺满宽度、高于 10px、纯色不透明」的元素时跳过它,
18
+ * 并改用这个颜色延伸到状态栏。所以顶栏要 sticky + 不透明底色,不能是普通的流式元素。
19
+ */
20
+ background: var(--bg);
21
+ position: sticky;
22
+ top: 0;
23
+ z-index: 5;
24
+ }
25
+
26
+ /* 保险:主屏 PWA 里再在最顶端放一条 fixed 的纯色细条(同样是为了让 WebKit 跳过边缘模糊)。
27
+ 层级低于顶栏和侧栏,和顶栏同色,看不出来 */
28
+ .edge-strip {
29
+ display: none;
30
+ }
31
+
32
+ @media (display-mode: standalone) and (max-width: 768px) {
33
+ .edge-strip {
34
+ display: block;
35
+ position: fixed;
36
+ top: 0;
37
+ left: 0;
38
+ right: 0;
39
+ height: max(12px, env(safe-area-inset-top));
40
+ background: var(--bg);
41
+ pointer-events: none;
42
+ z-index: 4;
43
+ }
44
+ }
45
+
46
+ /* 顶栏下沿:渐变 + 模糊,聊天内容滚上去时柔和地消失在顶栏后面 */
47
+ /*
48
+ * 顶栏浮在聊天内容上面:内容一直滚到窗口顶端,经过顶栏时先变模糊、再淡出到底色,
49
+ * 没有分界线(参考 Claude 桌面端)。顶栏自身比按钮行高出一截,多出来的部分就是过渡带。
50
+ * iOS 主屏 PWA 例外,见下面的媒体查询。
51
+ */
52
+ .topbar {
53
+ position: absolute;
54
+ left: 0;
55
+ right: 0;
56
+ padding-bottom: 20px;
57
+ background: linear-gradient(
58
+ to bottom,
59
+ var(--bg) 0%,
60
+ var(--bg) calc(100% - 22px),
61
+ color-mix(in srgb, var(--bg) 75%, transparent) calc(100% - 13px),
62
+ color-mix(in srgb, var(--bg) 30%, transparent) calc(100% - 5px),
63
+ color-mix(in srgb, var(--bg) 0%, transparent) 100%
64
+ );
65
+ pointer-events: none;
66
+ }
67
+
68
+ .topbar > * {
69
+ pointer-events: auto;
70
+ }
71
+
72
+ /* 模糊层:上实下虚,越往下越清楚,和渐变叠在一起 */
73
+ .topbar::before {
74
+ content: "";
75
+ position: absolute;
76
+ inset: 0;
77
+ z-index: -1;
78
+ -webkit-backdrop-filter: blur(10px);
79
+ backdrop-filter: blur(10px);
80
+ -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent);
81
+ mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent);
82
+ pointer-events: none;
83
+ }
84
+
85
+ /* 内容让出顶栏按钮行的高度(.main 前缀是为了压过手机端的 padding 简写) */
86
+ .main .messages-inner {
87
+ padding-top: calc(72px + env(safe-area-inset-top));
88
+ }
89
+
90
+ .messages {
91
+ scroll-padding-top: calc(72px + env(safe-area-inset-top));
92
+ }
93
+
94
+ /* iOS 主屏 PWA:顶端必须是不透明的 sticky 元素才能避开系统的边缘模糊,回到普通顶栏 + 下方淡出 */
95
+ @media (display-mode: standalone) and (max-width: 768px) {
96
+ .topbar {
97
+ position: sticky;
98
+ padding-bottom: 8px;
99
+ background: var(--bg);
100
+ }
101
+
102
+ .topbar::before {
103
+ display: none;
104
+ }
105
+
106
+ .topbar::after {
107
+ content: "";
108
+ position: absolute;
109
+ left: 0;
110
+ right: 0;
111
+ top: 100%;
112
+ height: 24px;
113
+ pointer-events: none;
114
+ background: linear-gradient(
115
+ to bottom,
116
+ var(--bg),
117
+ color-mix(in srgb, var(--bg) 60%, transparent) 40%,
118
+ color-mix(in srgb, var(--bg) 0%, transparent)
119
+ );
120
+ }
121
+
122
+ .main .messages-inner {
123
+ padding-top: 16px;
124
+ }
125
+ }
126
+
127
+ .topbar .menu-btn {
128
+ display: none;
129
+ }
130
+
131
+ .icon-btn {
132
+ width: 36px;
133
+ height: 36px;
134
+ display: inline-grid;
135
+ place-items: center;
136
+ border-radius: 8px;
137
+ flex-shrink: 0;
138
+ color: var(--fg-soft);
139
+ }
140
+
141
+ .icon-btn:hover {
142
+ background: var(--bg-hover);
143
+ }
144
+
145
+ .icon-btn svg {
146
+ width: 20px;
147
+ height: 20px;
148
+ }
149
+
150
+ .top-title {
151
+ flex: 1;
152
+ min-width: 0;
153
+ display: flex;
154
+ align-items: center;
155
+ gap: 6px;
156
+ }
157
+
158
+ .top-title .conv-name {
159
+ font-size: 15px;
160
+ font-weight: 500;
161
+ overflow: hidden;
162
+ text-overflow: ellipsis;
163
+ white-space: nowrap;
164
+ padding: 0 4px;
165
+ }
166
+
167
+ .pill {
168
+ display: inline-flex;
169
+ align-items: center;
170
+ gap: 4px;
171
+ padding: 5px 10px;
172
+ border-radius: 999px;
173
+ font-size: 14px;
174
+ font-weight: 500;
175
+ max-width: 100%;
176
+ white-space: nowrap;
177
+ }
178
+
179
+ .pill:hover {
180
+ background: var(--bg-hover);
181
+ }
182
+
183
+
184
+ .pill .sub {
185
+ color: var(--fg-faint);
186
+ font-weight: 400;
187
+ overflow: hidden;
188
+ text-overflow: ellipsis;
189
+ }
190
+
191
+ .pill svg {
192
+ width: 14px;
193
+ height: 14px;
194
+ flex-shrink: 0;
195
+ }
196
+
197
+ /* 桌面端(Agent Loft for macOS)里:顶栏和侧栏顶部按住能拖动窗口,里面的按钮照常能点 */
198
+ .in-desktop .topbar,
199
+ .in-desktop .side-top {
200
+ -webkit-app-region: drag;
201
+ }
202
+ .in-desktop .topbar :is(button, a, input, select, textarea, label, .conv-name),
203
+ .in-desktop .side-top :is(button, a, input, select, textarea, label) {
204
+ -webkit-app-region: no-drag;
205
+ }
package/web/index.html CHANGED
@@ -14,7 +14,18 @@
14
14
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
15
15
  <link rel="stylesheet" href="/vendor/hljs-light.css" media="(prefers-color-scheme: light)" />
16
16
  <link rel="stylesheet" href="/vendor/hljs-dark.css" media="(prefers-color-scheme: dark)" />
17
- <link rel="stylesheet" href="/style.css" />
17
+ <!-- 顺序就是层叠顺序,不要随意调换 -->
18
+ <link rel="stylesheet" href="/css/base.css" />
19
+ <link rel="stylesheet" href="/css/sidebar.css" />
20
+ <link rel="stylesheet" href="/css/topbar.css" />
21
+ <link rel="stylesheet" href="/css/messages.css" />
22
+ <link rel="stylesheet" href="/css/loader.css" />
23
+ <link rel="stylesheet" href="/css/composer.css" />
24
+ <link rel="stylesheet" href="/css/overlay.css" />
25
+ <link rel="stylesheet" href="/css/settings.css" />
26
+ <link rel="stylesheet" href="/css/schedule.css" />
27
+ <link rel="stylesheet" href="/css/tasks.css" />
28
+ <link rel="stylesheet" href="/css/mobile.css" />
18
29
  </head>
19
30
  <body>
20
31
  <div id="app"><div class="boot"><span class="loft-loader xl" role="img" aria-label="加载中"><svg viewBox="96 56 320 388" aria-hidden="true"><defs><linearGradient id="boot-g" gradientUnits="userSpaceOnUse" x1="104" y1="104" x2="408" y2="408"><stop offset="0" stop-color="#3b82f6"/><stop offset="1" stop-color="#06b6d4"/></linearGradient></defs><path class="ll-shadow" d="M256 296L404 364L256 432L108 364Z" fill="url(#boot-g)"/><path class="ll-plate" d="M256 252L404 320L256 388L108 320Z" fill="url(#boot-g)"/><path class="ll-star ll-l" d="M152 186C158.46 217.54 158.46 217.54 190 224C158.46 230.46 158.46 230.46 152 262C145.54 230.46 145.54 230.46 114 224C145.54 217.54 145.54 217.54 152 186Z" fill="url(#boot-g)"/><path class="ll-star ll-c" d="M256 82C268.92 145.08 268.92 145.08 332 158C268.92 170.92 268.92 170.92 256 234C243.08 170.92 243.08 170.92 180 158C243.08 145.08 243.08 145.08 256 82Z" fill="url(#boot-g)"/><path class="ll-star ll-r" d="M360 186C366.46 217.54 366.46 217.54 398 224C366.46 230.46 366.46 230.46 360 262C353.54 230.46 353.54 230.46 322 224C353.54 217.54 353.54 217.54 360 186Z" fill="url(#boot-g)"/></svg></span></div></div>
package/web/lib/api.js ADDED
@@ -0,0 +1,26 @@
1
+ import { showAuth } from "../views/auth.js";
2
+
3
+ export async function api(path, opts = {}) {
4
+ const res = await fetch(path, {
5
+ method: opts.method ?? "GET",
6
+ credentials: "same-origin",
7
+ headers: opts.body !== undefined && !(opts.body instanceof Blob) ? { "Content-Type": "application/json" } : opts.headers,
8
+ body: opts.body === undefined ? undefined : opts.body instanceof Blob ? opts.body : JSON.stringify(opts.body),
9
+ });
10
+ let data = null;
11
+ try {
12
+ data = await res.json();
13
+ } catch {
14
+ // 没有 body
15
+ }
16
+ if (res.status === 401 && !path.startsWith("/api/me") && !path.startsWith("/api/login")) {
17
+ showAuth();
18
+ throw new Error("请先登录");
19
+ }
20
+ if (!res.ok) {
21
+ const err = new Error(data?.error ?? `请求失败(${res.status})`);
22
+ err.status = res.status;
23
+ throw err;
24
+ }
25
+ return data;
26
+ }