@snack-kit/lib 0.1.0 → 0.3.0

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.
@@ -0,0 +1,866 @@
1
+ import { Get, Context } from './chunk-JQYH5FWE.js';
2
+
3
+ // src/debugger/debugger.ts
4
+ var STORAGE_PREFIX = "__snackkit_debugger__";
5
+ var KEY_GATEWAY = `${STORAGE_PREFIX}gateway`;
6
+ var KEY_TYPE = `${STORAGE_PREFIX}type`;
7
+ var KEY_SERVER = `${STORAGE_PREFIX}server`;
8
+ function isChinese() {
9
+ return /^zh/i.test(navigator.language ?? "");
10
+ }
11
+ var T = {
12
+ gateway: () => isChinese() ? "\u7F51\u5173" : "Gateway",
13
+ tag: () => isChinese() ? "\u6807\u7B7E" : "Tag",
14
+ server: () => isChinese() ? "\u670D\u52A1" : "Server",
15
+ ctx: () => isChinese() ? "\u4E0A\u4E0B\u6587" : "Ctx",
16
+ ctxLabel: () => isChinese() ? "\u4E0A\u4E0B\u6587 \u2014 \u70B9\u51FB\u884C\u590D\u5236 key" : "Ctx \u2014 click row to copy key",
17
+ search: () => isChinese() ? "\u641C\u7D22..." : "Search...",
18
+ noMatch: () => isChinese() ? "\u65E0\u5339\u914D\u9879" : "No results",
19
+ allTags: () => isChinese() ? "\u5168\u90E8\u6807\u7B7E" : "All tags",
20
+ selectGateway: () => isChinese() ? "\u9009\u62E9\u7F51\u5173" : "Select gateway",
21
+ selectServer: () => isChinese() ? "\u9009\u62E9\u670D\u52A1" : "Select server",
22
+ filterCtx: () => isChinese() ? "\u8FC7\u6EE4\u8DEF\u7531..." : "Filter routes...",
23
+ selectFirst: () => isChinese() ? "\u2190 \u8BF7\u5148\u9009\u62E9\u670D\u52A1" : "\u2190 Select a server first",
24
+ loading: () => isChinese() ? "\u52A0\u8F7D\u670D\u52A1\u5217\u8868..." : "Loading...",
25
+ loadFailed: (m) => isChinese() ? `\u52A0\u8F7D\u5931\u8D25: ${m}` : `Failed: ${m}`,
26
+ loaded: (n) => isChinese() ? `\u5DF2\u52A0\u8F7D ${n} \u4E2A\u670D\u52A1` : `${n} servers loaded`,
27
+ switching: () => isChinese() ? "\u5207\u6362\u670D\u52A1\u4E2D..." : "Switching...",
28
+ switchFailed: (m) => isChinese() ? `\u5207\u6362\u5931\u8D25: ${m}` : `Failed: ${m}`,
29
+ routes: (name, n) => isChinese() ? `${name} \xB7 ${n} \u6761\u8DEF\u7531` : `${name} \xB7 ${n} routes`,
30
+ copied: (k) => isChinese() ? `\u5DF2\u590D\u5236: ${k}` : `Copied: ${k}`,
31
+ copyAll: () => isChinese() ? "\u590D\u5236\u5168\u90E8 JSON" : "Copy all JSON",
32
+ copyAllDone: () => isChinese() ? "\u5DF2\u590D\u5236\u4E3A JSON" : "Copied as JSON"
33
+ };
34
+ var PANEL_STYLE = `
35
+ #__snackkit_debugger__ {
36
+ position: fixed;
37
+ bottom: 20px;
38
+ right: 20px;
39
+ z-index: 99999;
40
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
41
+ font-size: 13px;
42
+ color: #b8ccec;
43
+ user-select: none;
44
+ }
45
+
46
+ /* \u2500\u2500 \u6D6E\u52A8\u6309\u94AE \u2500\u2500 */
47
+ #__snackkit_debugger__ .dbg-toggle {
48
+ width: 42px;
49
+ height: 42px;
50
+ border-radius: 50%;
51
+ background: linear-gradient(135deg, #4f8ef7, #6d6ff5);
52
+ border: none;
53
+ cursor: pointer;
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: center;
57
+ box-shadow: 0 4px 14px rgba(79,142,247,0.45);
58
+ margin-left: auto;
59
+ font-size: 20px;
60
+ transition: transform 0.15s, box-shadow 0.15s;
61
+ }
62
+ #__snackkit_debugger__ .dbg-toggle:hover {
63
+ transform: scale(1.08);
64
+ box-shadow: 0 6px 20px rgba(79,142,247,0.55);
65
+ }
66
+ #__snackkit_debugger__ .dbg-toggle:active { transform: scale(0.94); }
67
+
68
+ /* \u2500\u2500 \u9762\u677F\u4E3B\u4F53 \u2500\u2500 */
69
+ #__snackkit_debugger__ .dbg-panel {
70
+ background: #1e2638;
71
+ border: 1px solid #2d3a55;
72
+ border-radius: 12px;
73
+ width: 440px;
74
+ margin-bottom: 10px;
75
+ box-shadow: 0 8px 32px rgba(0,0,0,0.45), 0 0 0 1px rgba(255,255,255,0.05);
76
+ overflow: visible;
77
+ opacity: 0;
78
+ transform: translateY(8px) scale(0.98);
79
+ pointer-events: none;
80
+ transition: opacity 0.18s ease, transform 0.18s ease;
81
+ }
82
+ #__snackkit_debugger__ .dbg-panel.open {
83
+ opacity: 1;
84
+ transform: translateY(0) scale(1);
85
+ pointer-events: auto;
86
+ }
87
+
88
+ /* \u2500\u2500 \u9762\u677F Header \u2500\u2500 */
89
+ #__snackkit_debugger__ .dbg-header {
90
+ display: flex;
91
+ align-items: center;
92
+ justify-content: space-between;
93
+ padding: 10px 14px;
94
+ background: #171e2e;
95
+ border-bottom: 1px solid #2d3a55;
96
+ border-radius: 12px 12px 0 0;
97
+ }
98
+ #__snackkit_debugger__ .dbg-header-left {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 7px;
102
+ }
103
+ #__snackkit_debugger__ .dbg-header-dot {
104
+ width: 7px;
105
+ height: 7px;
106
+ border-radius: 50%;
107
+ background: #34d399;
108
+ box-shadow: 0 0 6px rgba(52,211,153,0.6);
109
+ }
110
+ #__snackkit_debugger__ .dbg-header-dot.loading {
111
+ background: #fbbf24;
112
+ box-shadow: 0 0 6px rgba(251,191,36,0.6);
113
+ animation: dbg-pulse 1s ease-in-out infinite;
114
+ }
115
+ #__snackkit_debugger__ .dbg-header-dot.err {
116
+ background: #f87171;
117
+ box-shadow: 0 0 6px rgba(248,113,113,0.6);
118
+ }
119
+ #__snackkit_debugger__ .dbg-header-title {
120
+ font-size: 12px;
121
+ font-weight: 600;
122
+ color: #dce8fa;
123
+ letter-spacing: 0.03em;
124
+ }
125
+ #__snackkit_debugger__ .dbg-header-meta {
126
+ font-size: 11px;
127
+ color: #4d6080;
128
+ }
129
+
130
+ /* \u2500\u2500 \u9762\u677F\u5185\u5BB9\u533A \u2500\u2500 */
131
+ #__snackkit_debugger__ .dbg-body {
132
+ padding: 12px 14px;
133
+ display: flex;
134
+ flex-direction: column;
135
+ gap: 10px;
136
+ height: 420px;
137
+ overflow: visible;
138
+ }
139
+
140
+ /* \u2500\u2500 \u5B57\u6BB5\u884C \u2500\u2500 */
141
+ #__snackkit_debugger__ .dbg-field {
142
+ display: flex;
143
+ flex-direction: column;
144
+ gap: 5px;
145
+ }
146
+ #__snackkit_debugger__ .dbg-field-header {
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: space-between;
150
+ }
151
+ #__snackkit_debugger__ .dbg-label {
152
+ font-size: 10px;
153
+ font-weight: 600;
154
+ color: #5e72a0;
155
+ text-transform: uppercase;
156
+ letter-spacing: 0.07em;
157
+ }
158
+
159
+ /* \u2500\u2500 \u4E00\u952E\u590D\u5236\u5168\u90E8 JSON \u6309\u94AE \u2500\u2500 */
160
+ #__snackkit_debugger__ .dbg-copy-all {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 4px;
164
+ background: transparent;
165
+ border: 1px solid #2d3a55;
166
+ border-radius: 4px;
167
+ padding: 2px 7px;
168
+ color: #5e72a0;
169
+ font-size: 10px;
170
+ font-family: inherit;
171
+ cursor: pointer;
172
+ transition: border-color 0.15s, color 0.15s, background 0.15s;
173
+ }
174
+ #__snackkit_debugger__ .dbg-copy-all:hover {
175
+ border-color: #4f8ef7;
176
+ color: #7aacfa;
177
+ background: rgba(79,142,247,0.08);
178
+ }
179
+ #__snackkit_debugger__ .dbg-copy-all.done {
180
+ border-color: #34d399;
181
+ color: #34d399;
182
+ }
183
+
184
+ /* \u2500\u2500 \u81EA\u5B9A\u4E49\u4E0B\u62C9 \u2500\u2500 */
185
+ #__snackkit_debugger__ .dbg-select {
186
+ position: relative;
187
+ }
188
+ #__snackkit_debugger__ .dbg-select-trigger {
189
+ display: flex;
190
+ align-items: center;
191
+ justify-content: space-between;
192
+ gap: 6px;
193
+ background: #252f45;
194
+ border: 1px solid #2d3a55;
195
+ border-radius: 6px;
196
+ padding: 6px 10px;
197
+ cursor: pointer;
198
+ transition: border-color 0.15s, background 0.15s;
199
+ min-height: 32px;
200
+ }
201
+ #__snackkit_debugger__ .dbg-select-trigger:hover { border-color: #3d5070; background: #2d3a55; }
202
+ #__snackkit_debugger__ .dbg-select.open .dbg-select-trigger {
203
+ border-color: #4f8ef7;
204
+ background: #2d3a55;
205
+ }
206
+ #__snackkit_debugger__ .dbg-select-val {
207
+ flex: 1;
208
+ overflow: hidden;
209
+ text-overflow: ellipsis;
210
+ white-space: nowrap;
211
+ font-size: 12px;
212
+ color: #b8ccec;
213
+ }
214
+ #__snackkit_debugger__ .dbg-select-val.placeholder { color: #3d5070; }
215
+ #__snackkit_debugger__ .dbg-select-arrow {
216
+ color: #3d5070;
217
+ font-size: 10px;
218
+ flex-shrink: 0;
219
+ transition: transform 0.15s, color 0.15s;
220
+ }
221
+ #__snackkit_debugger__ .dbg-select.open .dbg-select-arrow {
222
+ transform: rotate(180deg);
223
+ color: #4f8ef7;
224
+ }
225
+ #__snackkit_debugger__ .dbg-select-dropdown {
226
+ position: absolute;
227
+ bottom: calc(100% + 4px);
228
+ left: 0;
229
+ right: 0;
230
+ background: #1e2638;
231
+ border: 1px solid #2d3a55;
232
+ border-radius: 8px;
233
+ box-shadow: 0 -8px 24px rgba(0,0,0,0.35);
234
+ overflow: hidden;
235
+ opacity: 0;
236
+ transform: translateY(4px);
237
+ pointer-events: none;
238
+ transition: opacity 0.14s, transform 0.14s;
239
+ z-index: 100000;
240
+ }
241
+ #__snackkit_debugger__ .dbg-select.open .dbg-select-dropdown {
242
+ opacity: 1;
243
+ transform: translateY(0);
244
+ pointer-events: auto;
245
+ }
246
+ #__snackkit_debugger__ .dbg-select-search {
247
+ display: flex;
248
+ align-items: center;
249
+ gap: 6px;
250
+ padding: 7px 10px;
251
+ border-bottom: 1px solid #2d3a55;
252
+ }
253
+ #__snackkit_debugger__ .dbg-select-search-icon { color: #3d5070; font-size: 11px; flex-shrink: 0; }
254
+ #__snackkit_debugger__ .dbg-select-search input {
255
+ flex: 1;
256
+ background: transparent;
257
+ border: none;
258
+ outline: none;
259
+ color: #b8ccec;
260
+ font-size: 12px;
261
+ font-family: inherit;
262
+ }
263
+ #__snackkit_debugger__ .dbg-select-search input::placeholder { color: #3d5070; }
264
+ #__snackkit_debugger__ .dbg-select-list {
265
+ max-height: 160px;
266
+ overflow-y: auto;
267
+ }
268
+ #__snackkit_debugger__ .dbg-select-list::-webkit-scrollbar { width: 3px; }
269
+ #__snackkit_debugger__ .dbg-select-list::-webkit-scrollbar-track { background: transparent; }
270
+ #__snackkit_debugger__ .dbg-select-list::-webkit-scrollbar-thumb { background: #2d3a55; border-radius: 2px; }
271
+ #__snackkit_debugger__ .dbg-select-option {
272
+ padding: 7px 10px;
273
+ cursor: pointer;
274
+ font-size: 12px;
275
+ color: #8aa4c8;
276
+ transition: background 0.1s, color 0.1s;
277
+ overflow: hidden;
278
+ text-overflow: ellipsis;
279
+ white-space: nowrap;
280
+ }
281
+ #__snackkit_debugger__ .dbg-select-option:hover { background: #2d3a55; color: #dce8fa; }
282
+ #__snackkit_debugger__ .dbg-select-option.active { color: #7aacfa; background: #1e3060; }
283
+ #__snackkit_debugger__ .dbg-select-option.hidden { display: none; }
284
+ #__snackkit_debugger__ .dbg-select-empty {
285
+ padding: 12px 10px;
286
+ font-size: 12px;
287
+ color: #3d5070;
288
+ text-align: center;
289
+ }
290
+
291
+ /* \u2500\u2500 \u8DEF\u7531\u5217\u8868 \u2500\u2500 */
292
+ #__snackkit_debugger__ .dbg-ctx-field {
293
+ flex: 1;
294
+ min-height: 0;
295
+ display: flex;
296
+ flex-direction: column;
297
+ gap: 5px;
298
+ }
299
+ #__snackkit_debugger__ .dbg-ctx-wrap {
300
+ flex: 1;
301
+ min-height: 0;
302
+ border: 1px solid #2d3a55;
303
+ border-radius: 6px;
304
+ overflow: hidden;
305
+ background: #252f45;
306
+ display: flex;
307
+ flex-direction: column;
308
+ }
309
+ #__snackkit_debugger__ .dbg-ctx-search {
310
+ display: flex;
311
+ align-items: center;
312
+ gap: 6px;
313
+ padding: 6px 10px;
314
+ border-bottom: 1px solid #2d3a55;
315
+ background: #1e2638;
316
+ }
317
+ #__snackkit_debugger__ .dbg-ctx-search input {
318
+ flex: 1;
319
+ background: transparent;
320
+ border: none;
321
+ outline: none;
322
+ color: #b8ccec;
323
+ font-size: 12px;
324
+ font-family: inherit;
325
+ }
326
+ #__snackkit_debugger__ .dbg-ctx-search input::placeholder { color: #3d5070; }
327
+ #__snackkit_debugger__ .dbg-ctx-list {
328
+ flex: 1;
329
+ min-height: 0;
330
+ overflow-y: auto;
331
+ }
332
+ #__snackkit_debugger__ .dbg-ctx-list::-webkit-scrollbar { width: 3px; }
333
+ #__snackkit_debugger__ .dbg-ctx-list::-webkit-scrollbar-track { background: transparent; }
334
+ #__snackkit_debugger__ .dbg-ctx-list::-webkit-scrollbar-thumb { background: #2d3a55; border-radius: 2px; }
335
+ #__snackkit_debugger__ .dbg-ctx-item {
336
+ padding: 5px 10px;
337
+ cursor: pointer;
338
+ display: grid;
339
+ grid-template-columns: minmax(80px, 148px) 1fr auto;
340
+ gap: 8px;
341
+ align-items: center;
342
+ border-bottom: 1px solid #1e2638;
343
+ transition: background 0.1s;
344
+ }
345
+ #__snackkit_debugger__ .dbg-ctx-item:last-child { border-bottom: none; }
346
+ #__snackkit_debugger__ .dbg-ctx-item:hover { background: #2d3a55; }
347
+ #__snackkit_debugger__ .dbg-ctx-item.hidden { display: none; }
348
+ #__snackkit_debugger__ .dbg-ctx-key {
349
+ font-size: 12px;
350
+ color: #7aacfa;
351
+ overflow: hidden;
352
+ text-overflow: ellipsis;
353
+ white-space: nowrap;
354
+ }
355
+ #__snackkit_debugger__ .dbg-ctx-val {
356
+ font-size: 11px;
357
+ color: #4d6080;
358
+ overflow: hidden;
359
+ text-overflow: ellipsis;
360
+ white-space: nowrap;
361
+ transition: color 0.1s;
362
+ }
363
+ #__snackkit_debugger__ .dbg-ctx-item:hover .dbg-ctx-val { color: #7a90b0; }
364
+ #__snackkit_debugger__ .dbg-ctx-copy {
365
+ font-size: 11px;
366
+ color: #3d5070;
367
+ opacity: 0;
368
+ flex-shrink: 0;
369
+ transition: opacity 0.1s, color 0.1s;
370
+ }
371
+ #__snackkit_debugger__ .dbg-ctx-item:hover .dbg-ctx-copy { opacity: 1; color: #5e72a0; }
372
+ #__snackkit_debugger__ .dbg-ctx-item.copied .dbg-ctx-copy { color: #34d399; opacity: 1; }
373
+ #__snackkit_debugger__ .dbg-ctx-empty {
374
+ padding: 16px 10px;
375
+ font-size: 12px;
376
+ color: #3d5070;
377
+ text-align: center;
378
+ }
379
+
380
+ /* \u2500\u2500 \u5E95\u90E8\u72B6\u6001\u680F \u2500\u2500 */
381
+ #__snackkit_debugger__ .dbg-footer {
382
+ display: flex;
383
+ align-items: center;
384
+ gap: 6px;
385
+ padding: 7px 14px;
386
+ background: #171e2e;
387
+ border-top: 1px solid #2d3a55;
388
+ border-radius: 0 0 12px 12px;
389
+ font-size: 11px;
390
+ color: #4d6080;
391
+ min-height: 30px;
392
+ }
393
+ #__snackkit_debugger__ .dbg-footer.ok { color: #34d399; }
394
+ #__snackkit_debugger__ .dbg-footer.err { color: #f87171; }
395
+ #__snackkit_debugger__ .dbg-footer-dot {
396
+ width: 4px;
397
+ height: 4px;
398
+ border-radius: 50%;
399
+ background: currentColor;
400
+ flex-shrink: 0;
401
+ }
402
+
403
+ @keyframes dbg-pulse {
404
+ 0%, 100% { opacity: 1; }
405
+ 50% { opacity: 0.35; }
406
+ }
407
+ `;
408
+ var SearchSelect = class {
409
+ constructor(onChange) {
410
+ this.options = [];
411
+ this.selected = "";
412
+ this.onChange = onChange;
413
+ this.el = document.createElement("div");
414
+ this.el.className = "dbg-select";
415
+ this.trigger = document.createElement("div");
416
+ this.trigger.className = "dbg-select-trigger";
417
+ this.valEl = document.createElement("span");
418
+ this.valEl.className = "dbg-select-val placeholder";
419
+ const arrow = document.createElement("span");
420
+ arrow.className = "dbg-select-arrow";
421
+ arrow.textContent = "\u25BE";
422
+ this.trigger.appendChild(this.valEl);
423
+ this.trigger.appendChild(arrow);
424
+ this.dropdown = document.createElement("div");
425
+ this.dropdown.className = "dbg-select-dropdown";
426
+ const searchWrap = document.createElement("div");
427
+ searchWrap.className = "dbg-select-search";
428
+ const searchIcon = document.createElement("span");
429
+ searchIcon.className = "dbg-select-search-icon";
430
+ searchIcon.textContent = "\u2315";
431
+ this.searchInput = document.createElement("input");
432
+ this.searchInput.type = "text";
433
+ this.searchInput.placeholder = T.search();
434
+ searchWrap.appendChild(searchIcon);
435
+ searchWrap.appendChild(this.searchInput);
436
+ this.listEl = document.createElement("div");
437
+ this.listEl.className = "dbg-select-list";
438
+ this.dropdown.appendChild(searchWrap);
439
+ this.dropdown.appendChild(this.listEl);
440
+ this.el.appendChild(this.trigger);
441
+ this.el.appendChild(this.dropdown);
442
+ this.bindEvents();
443
+ }
444
+ bindEvents() {
445
+ this.trigger.addEventListener("click", (e) => {
446
+ e.stopPropagation();
447
+ const isOpen = this.el.classList.contains("open");
448
+ document.querySelectorAll("#__snackkit_debugger__ .dbg-select.open").forEach((el) => {
449
+ if (el !== this.el) el.classList.remove("open");
450
+ });
451
+ this.el.classList.toggle("open", !isOpen);
452
+ if (!isOpen) {
453
+ this.searchInput.value = "";
454
+ this.filterOptions("");
455
+ setTimeout(() => this.searchInput.focus(), 50);
456
+ }
457
+ });
458
+ this.searchInput.addEventListener("input", () => {
459
+ this.filterOptions(this.searchInput.value);
460
+ });
461
+ this.searchInput.addEventListener("click", (e) => e.stopPropagation());
462
+ document.addEventListener("click", () => {
463
+ this.el.classList.remove("open");
464
+ });
465
+ }
466
+ filterOptions(query) {
467
+ const q = query.toLowerCase();
468
+ let visibleCount = 0;
469
+ this.listEl.querySelectorAll(".dbg-select-option").forEach((opt) => {
470
+ const match = !q || opt.textContent.toLowerCase().includes(q);
471
+ opt.classList.toggle("hidden", !match);
472
+ if (match) visibleCount++;
473
+ });
474
+ let emptyEl = this.listEl.querySelector(".dbg-select-empty");
475
+ if (visibleCount === 0) {
476
+ if (!emptyEl) {
477
+ emptyEl = document.createElement("div");
478
+ emptyEl.className = "dbg-select-empty";
479
+ emptyEl.textContent = T.noMatch();
480
+ this.listEl.appendChild(emptyEl);
481
+ }
482
+ } else {
483
+ emptyEl?.remove();
484
+ }
485
+ }
486
+ /** 更新选项列表 */
487
+ setOptions(options, placeholder) {
488
+ this.options = options;
489
+ this.listEl.innerHTML = "";
490
+ this.valEl.textContent = placeholder;
491
+ this.valEl.className = "dbg-select-val placeholder";
492
+ this.selected = "";
493
+ options.forEach(({ value, label }) => {
494
+ const opt = document.createElement("div");
495
+ opt.className = "dbg-select-option";
496
+ opt.dataset["value"] = value;
497
+ opt.textContent = label;
498
+ opt.setAttribute("title", label);
499
+ opt.addEventListener("click", (e) => {
500
+ e.stopPropagation();
501
+ this.select(value, label);
502
+ this.el.classList.remove("open");
503
+ });
504
+ this.listEl.appendChild(opt);
505
+ });
506
+ }
507
+ /** 选中某个值(不触发 onChange) */
508
+ setValue(value) {
509
+ const opt = this.options.find((o) => o.value === value);
510
+ if (opt) this.select(opt.value, opt.label, false);
511
+ }
512
+ select(value, label, emit = true) {
513
+ this.selected = value;
514
+ this.valEl.textContent = label;
515
+ this.valEl.className = "dbg-select-val";
516
+ this.listEl.querySelectorAll(".dbg-select-option").forEach((el) => {
517
+ el.classList.toggle("active", el.dataset["value"] === value);
518
+ });
519
+ if (emit) this.onChange(value);
520
+ }
521
+ getValue() {
522
+ return this.selected;
523
+ }
524
+ getElement() {
525
+ return this.el;
526
+ }
527
+ };
528
+ var Debugger = class _Debugger {
529
+ constructor(options) {
530
+ this.servers = [];
531
+ this.currentCtxMap = {};
532
+ this.options = options;
533
+ this.gateways = Array.isArray(options.gateways) ? options.gateways : [options.gateways];
534
+ }
535
+ /**
536
+ * 初始化并渲染调试面板
537
+ *
538
+ * 调用后在页面右下角插入浮动按钮,点击展开调试面板。
539
+ * 面板支持:选择调试网关、切换目标服务、查看并复制路由 ctx。
540
+ * 网关/服务选择通过 `localStorage` 持久化,刷新后自动恢复。
541
+ *
542
+ * @param options 配置项
543
+ *
544
+ * @example 单网关
545
+ * ```ts
546
+ * import { Debugger } from '@snack-kit/lib/debugger'
547
+ *
548
+ * await Debugger.init({
549
+ * gateways: 'http://dev-gateway.example.com',
550
+ * })
551
+ * ```
552
+ *
553
+ * @example 多网关(开发/测试环境切换)
554
+ * ```ts
555
+ * await Debugger.init({
556
+ * gateways: [
557
+ * 'http://dev-gateway.example.com',
558
+ * 'http://test-gateway.example.com',
559
+ * ],
560
+ * timeout: 5000,
561
+ * })
562
+ * ```
563
+ *
564
+ * @example 仅在非生产环境加载
565
+ * ```ts
566
+ * if (import.meta.env.DEV) {
567
+ * const { Debugger } = await import('@snack-kit/lib/debugger')
568
+ * await Debugger.init({ gateways: 'http://dev-gateway.example.com' })
569
+ * }
570
+ * ```
571
+ */
572
+ static async init(options) {
573
+ const instance = new _Debugger({ timeout: 1e4, ...options });
574
+ instance.injectStyle();
575
+ instance.buildDOM();
576
+ await instance.restoreState();
577
+ return instance;
578
+ }
579
+ /** 注入内联样式 */
580
+ injectStyle() {
581
+ if (document.getElementById("__snackkit_debugger_style__")) return;
582
+ const style = document.createElement("style");
583
+ style.id = "__snackkit_debugger_style__";
584
+ style.textContent = PANEL_STYLE;
585
+ document.head.appendChild(style);
586
+ }
587
+ /** 构建 DOM 结构 */
588
+ buildDOM() {
589
+ const root = document.createElement("div");
590
+ root.id = "__snackkit_debugger__";
591
+ const panel = document.createElement("div");
592
+ panel.className = "dbg-panel";
593
+ this.panelEl = panel;
594
+ const header = document.createElement("div");
595
+ header.className = "dbg-header";
596
+ const headerLeft = document.createElement("div");
597
+ headerLeft.className = "dbg-header-left";
598
+ this.headerDot = document.createElement("span");
599
+ this.headerDot.className = "dbg-header-dot loading";
600
+ const headerTitle = document.createElement("span");
601
+ headerTitle.className = "dbg-header-title";
602
+ headerTitle.textContent = "Snack Kit Debugger";
603
+ headerLeft.appendChild(this.headerDot);
604
+ headerLeft.appendChild(headerTitle);
605
+ header.appendChild(headerLeft);
606
+ panel.appendChild(header);
607
+ const body = document.createElement("div");
608
+ body.className = "dbg-body";
609
+ this.gwVersion = document.createElement("span");
610
+ this.gwVersion.className = "dbg-header-meta";
611
+ this.gwSelect = new SearchSelect((val) => this.onGatewayChange(val));
612
+ this.gwSelect.setOptions(this.gateways.map((g) => ({ value: g, label: g })), T.selectGateway());
613
+ body.appendChild(this.createFieldWithAction(T.gateway(), this.gwVersion, this.gwSelect.getElement()));
614
+ this.typeSelect = new SearchSelect((val) => this.onTypeChange(val));
615
+ body.appendChild(this.createField(T.tag(), this.typeSelect.getElement()));
616
+ this.serverSelect = new SearchSelect((val) => this.onServerChange(val));
617
+ body.appendChild(this.createField(T.server(), this.serverSelect.getElement()));
618
+ const ctxWrap = document.createElement("div");
619
+ ctxWrap.className = "dbg-ctx-wrap";
620
+ const ctxSearch = document.createElement("div");
621
+ ctxSearch.className = "dbg-ctx-search";
622
+ const ctxSearchIcon = document.createElement("span");
623
+ ctxSearchIcon.className = "dbg-select-search-icon";
624
+ ctxSearchIcon.textContent = "\u2315";
625
+ this.ctxSearchInput = document.createElement("input");
626
+ this.ctxSearchInput.type = "text";
627
+ this.ctxSearchInput.placeholder = T.filterCtx();
628
+ this.ctxSearchInput.addEventListener("input", () => this.filterCtx(this.ctxSearchInput.value));
629
+ ctxSearch.appendChild(ctxSearchIcon);
630
+ ctxSearch.appendChild(this.ctxSearchInput);
631
+ this.ctxList = document.createElement("div");
632
+ this.ctxList.className = "dbg-ctx-list";
633
+ ctxWrap.appendChild(ctxSearch);
634
+ ctxWrap.appendChild(this.ctxList);
635
+ this.copyAllBtn = document.createElement("button");
636
+ this.copyAllBtn.className = "dbg-copy-all";
637
+ this.copyAllBtn.textContent = T.copyAll();
638
+ this.copyAllBtn.setAttribute("title", isChinese() ? "\u5C06\u5168\u90E8\u4E0A\u4E0B\u6587\u590D\u5236\u4E3A JSON\uFF0C\u53EF\u4F5C\u4E3A Context.load({...}) \u7684\u53C2\u6570" : "Copy all ctx entries as JSON for Context.load({...})");
639
+ this.copyAllBtn.addEventListener("click", (e) => {
640
+ e.stopPropagation();
641
+ this.copyAllCtx();
642
+ });
643
+ const ctxField = this.createFieldWithAction(T.ctxLabel(), this.copyAllBtn, ctxWrap);
644
+ ctxField.classList.add("dbg-ctx-field");
645
+ body.appendChild(ctxField);
646
+ panel.appendChild(body);
647
+ const footer = document.createElement("div");
648
+ footer.className = "dbg-footer";
649
+ this.footerEl = footer;
650
+ panel.appendChild(footer);
651
+ const toggle = document.createElement("button");
652
+ toggle.className = "dbg-toggle";
653
+ toggle.textContent = "\u{1F41B}";
654
+ toggle.setAttribute("title", "Snack Debugger");
655
+ toggle.addEventListener("click", (e) => {
656
+ e.stopPropagation();
657
+ panel.classList.toggle("open");
658
+ });
659
+ root.appendChild(panel);
660
+ root.appendChild(toggle);
661
+ document.body.appendChild(root);
662
+ }
663
+ /** 创建普通字段行(label + content) */
664
+ createField(label, content) {
665
+ const field = document.createElement("div");
666
+ field.className = "dbg-field";
667
+ const lbl = document.createElement("div");
668
+ lbl.className = "dbg-label";
669
+ lbl.textContent = label;
670
+ field.appendChild(lbl);
671
+ field.appendChild(content);
672
+ return field;
673
+ }
674
+ /** 创建带操作按钮的字段行(label + action button + content) */
675
+ createFieldWithAction(label, action, content) {
676
+ const field = document.createElement("div");
677
+ field.className = "dbg-field";
678
+ const fieldHeader = document.createElement("div");
679
+ fieldHeader.className = "dbg-field-header";
680
+ const lbl = document.createElement("div");
681
+ lbl.className = "dbg-label";
682
+ lbl.textContent = label;
683
+ fieldHeader.appendChild(lbl);
684
+ fieldHeader.appendChild(action);
685
+ field.appendChild(fieldHeader);
686
+ field.appendChild(content);
687
+ return field;
688
+ }
689
+ /** 恢复上次持久化的状态 */
690
+ async restoreState() {
691
+ const savedGw = localStorage.getItem(KEY_GATEWAY);
692
+ const gw = savedGw && this.gateways.includes(savedGw) ? savedGw : this.gateways[0];
693
+ this.gwSelect.setValue(gw);
694
+ await this.loadServers(gw);
695
+ const savedType = localStorage.getItem(KEY_TYPE);
696
+ if (savedType) {
697
+ this.typeSelect.setValue(savedType);
698
+ this.renderServerOptions(savedType);
699
+ }
700
+ const savedServer = localStorage.getItem(KEY_SERVER);
701
+ if (savedServer && this.servers.find((s) => s.key === savedServer)) {
702
+ this.serverSelect.setValue(savedServer);
703
+ await this.applyServer(savedServer);
704
+ }
705
+ }
706
+ async onGatewayChange(gw) {
707
+ localStorage.setItem(KEY_GATEWAY, gw);
708
+ this.gwVersion.textContent = "";
709
+ await this.loadServers(gw);
710
+ }
711
+ onTypeChange(type) {
712
+ localStorage.setItem(KEY_TYPE, type);
713
+ this.renderServerOptions(type);
714
+ }
715
+ async onServerChange(key) {
716
+ localStorage.setItem(KEY_SERVER, key);
717
+ await this.applyServer(key);
718
+ }
719
+ /** 从网关加载服务列表 */
720
+ async loadServers(gwUrl) {
721
+ this.setHeader("loading");
722
+ this.setFooter(T.loading(), "");
723
+ const result = await Get(`${gwUrl}/web-debug/host/list`, {
724
+ cache: true,
725
+ timeout: this.options.timeout
726
+ });
727
+ if (result.error) {
728
+ this.setHeader("err");
729
+ this.setFooter(T.loadFailed(result.error.message), "err");
730
+ return;
731
+ }
732
+ this.servers = result.data ?? [];
733
+ this.renderTypeOptions();
734
+ this.setHeader("ok");
735
+ this.setFooter(T.loaded(this.servers.length), "ok");
736
+ }
737
+ /** 渲染标签选项 */
738
+ renderTypeOptions() {
739
+ const types = [...new Set(this.servers.map((s) => s.type).filter(Boolean))];
740
+ const opts = [{ value: "", label: T.allTags() }, ...types.map((t) => ({ value: t, label: t }))];
741
+ this.typeSelect.setOptions(opts, T.allTags());
742
+ this.typeSelect.setValue("");
743
+ this.renderServerOptions("");
744
+ }
745
+ /** 根据标签渲染服务选项 */
746
+ renderServerOptions(type) {
747
+ const filtered = type ? this.servers.filter((s) => s.type === type) : this.servers;
748
+ this.serverSelect.setOptions(
749
+ filtered.map((s) => ({ value: s.key, label: `${s.name} (${s.key})` })),
750
+ T.selectServer()
751
+ );
752
+ this.renderCtxList({});
753
+ }
754
+ /** 切换目标服务,重新加载 Context 路由映射 */
755
+ async applyServer(key) {
756
+ if (!key) {
757
+ this.renderCtxList({});
758
+ return;
759
+ }
760
+ const server = this.servers.find((s) => s.key === key);
761
+ if (!server) return;
762
+ this.setFooter(T.switching(), "");
763
+ try {
764
+ await Context.load(server.origin, this.options.timeout);
765
+ const res = await fetch(`${server.origin}/ngw/context`);
766
+ const raw = res.ok ? await res.json() : {};
767
+ const ctxMap = {};
768
+ for (const [k, v] of Object.entries(raw)) {
769
+ if (k !== "$info" && typeof v === "string") ctxMap[k] = v;
770
+ }
771
+ const info = Context.info;
772
+ if (info) this.gwVersion.textContent = `v${info.version}`;
773
+ this.renderCtxList(ctxMap);
774
+ const count = Object.keys(ctxMap).length;
775
+ this.setFooter(T.routes(server.name, count), "ok");
776
+ } catch (err) {
777
+ const msg = err instanceof Error ? err.message : String(err);
778
+ this.setFooter(T.switchFailed(msg), "err");
779
+ }
780
+ }
781
+ /** 渲染路由列表 */
782
+ renderCtxList(ctxMap) {
783
+ this.currentCtxMap = ctxMap;
784
+ this.ctxList.innerHTML = "";
785
+ this.ctxSearchInput.value = "";
786
+ const entries = Object.entries(ctxMap);
787
+ if (entries.length === 0) {
788
+ const empty = document.createElement("div");
789
+ empty.className = "dbg-ctx-empty";
790
+ empty.textContent = T.selectFirst();
791
+ this.ctxList.appendChild(empty);
792
+ return;
793
+ }
794
+ entries.forEach(([k, v]) => {
795
+ const item = document.createElement("div");
796
+ item.className = "dbg-ctx-item";
797
+ item.dataset["key"] = k;
798
+ const keySpan = document.createElement("span");
799
+ keySpan.className = "dbg-ctx-key";
800
+ keySpan.textContent = k;
801
+ keySpan.setAttribute("title", k);
802
+ const valSpan = document.createElement("span");
803
+ valSpan.className = "dbg-ctx-val";
804
+ valSpan.textContent = v;
805
+ valSpan.setAttribute("title", `${k} \u2192 ${v}`);
806
+ const copyIcon = document.createElement("span");
807
+ copyIcon.className = "dbg-ctx-copy";
808
+ copyIcon.textContent = "\u2398";
809
+ item.appendChild(keySpan);
810
+ item.appendChild(valSpan);
811
+ item.appendChild(copyIcon);
812
+ item.addEventListener("click", () => this.copyCtxItem(item, k));
813
+ this.ctxList.appendChild(item);
814
+ });
815
+ }
816
+ /** 过滤路由列表 */
817
+ filterCtx(query) {
818
+ const q = query.toLowerCase();
819
+ this.ctxList.querySelectorAll(".dbg-ctx-item").forEach((item) => {
820
+ const key = item.dataset["key"] ?? "";
821
+ item.classList.toggle("hidden", !!q && !key.toLowerCase().includes(q));
822
+ });
823
+ }
824
+ /** 复制单个 ctx key */
825
+ copyCtxItem(item, text) {
826
+ navigator.clipboard.writeText(text).then(() => {
827
+ item.classList.add("copied");
828
+ const icon = item.querySelector(".dbg-ctx-copy");
829
+ if (icon) icon.textContent = "\u2713";
830
+ setTimeout(() => {
831
+ item.classList.remove("copied");
832
+ const ic = item.querySelector(".dbg-ctx-copy");
833
+ if (ic) ic.textContent = "\u2398";
834
+ }, 1500);
835
+ this.setFooter(T.copied(text), "ok");
836
+ setTimeout(() => this.setFooter("", ""), 2e3);
837
+ });
838
+ }
839
+ /** 一键复制全部上下文为 JSON */
840
+ copyAllCtx() {
841
+ const entries = Object.keys(this.currentCtxMap);
842
+ if (entries.length === 0) return;
843
+ const json = JSON.stringify(this.currentCtxMap, null, 2);
844
+ navigator.clipboard.writeText(json).then(() => {
845
+ this.copyAllBtn.textContent = "\u2713 " + T.copyAllDone();
846
+ this.copyAllBtn.classList.add("done");
847
+ setTimeout(() => {
848
+ this.copyAllBtn.textContent = T.copyAll();
849
+ this.copyAllBtn.classList.remove("done");
850
+ }, 2e3);
851
+ this.setFooter(T.copyAllDone(), "ok");
852
+ setTimeout(() => this.setFooter("", ""), 2500);
853
+ });
854
+ }
855
+ setHeader(state) {
856
+ this.headerDot.className = `dbg-header-dot${state !== "ok" ? ` ${state}` : ""}`;
857
+ }
858
+ setFooter(msg, cls) {
859
+ this.footerEl.innerHTML = msg ? `<span class="dbg-footer-dot"></span>${msg}` : "";
860
+ this.footerEl.className = `dbg-footer${cls ? ` ${cls}` : ""}`;
861
+ }
862
+ };
863
+
864
+ export { Debugger };
865
+ //# sourceMappingURL=chunk-4DLFIN3C.js.map
866
+ //# sourceMappingURL=chunk-4DLFIN3C.js.map