hank9999 commited on
Commit
c00d86d
·
1 Parent(s): 4327a43

feat(tools): 添加 AWS Event Stream Viewer,支持解析和查看事件流

Browse files
Files changed (1) hide show
  1. tools/event-viewer.html +896 -0
tools/event-viewer.html ADDED
@@ -0,0 +1,896 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AWS Event Stream Viewer</title>
7
+ <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+ body {
14
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
15
+ background: #0d1117;
16
+ color: #c9d1d9;
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+ .container {
21
+ max-width: 1400px;
22
+ margin: 0 auto;
23
+ }
24
+ h1 {
25
+ color: #58a6ff;
26
+ margin-bottom: 20px;
27
+ font-size: 24px;
28
+ }
29
+ .input-section {
30
+ margin-bottom: 20px;
31
+ }
32
+ .input-section label {
33
+ display: block;
34
+ margin-bottom: 8px;
35
+ color: #8b949e;
36
+ font-size: 14px;
37
+ }
38
+ .input-controls {
39
+ display: flex;
40
+ gap: 10px;
41
+ margin-bottom: 10px;
42
+ flex-wrap: wrap;
43
+ }
44
+ .format-select {
45
+ padding: 8px 16px;
46
+ border: 1px solid #30363d;
47
+ border-radius: 6px;
48
+ background: #21262d;
49
+ color: #c9d1d9;
50
+ font-size: 14px;
51
+ }
52
+ textarea {
53
+ width: 100%;
54
+ height: 200px;
55
+ padding: 12px;
56
+ border: 1px solid #30363d;
57
+ border-radius: 6px;
58
+ background: #161b22;
59
+ color: #c9d1d9;
60
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
61
+ font-size: 13px;
62
+ resize: vertical;
63
+ }
64
+ textarea:focus {
65
+ outline: none;
66
+ border-color: #58a6ff;
67
+ }
68
+ .btn {
69
+ padding: 10px 20px;
70
+ border: none;
71
+ border-radius: 6px;
72
+ cursor: pointer;
73
+ font-size: 14px;
74
+ font-weight: 500;
75
+ transition: all 0.2s;
76
+ }
77
+ .btn-primary {
78
+ background: #238636;
79
+ color: white;
80
+ }
81
+ .btn-primary:hover {
82
+ background: #2ea043;
83
+ }
84
+ .btn-secondary {
85
+ background: #21262d;
86
+ color: #c9d1d9;
87
+ border: 1px solid #30363d;
88
+ }
89
+ .btn-secondary:hover {
90
+ background: #30363d;
91
+ }
92
+ .results {
93
+ margin-top: 20px;
94
+ }
95
+ .stats {
96
+ display: flex;
97
+ gap: 20px;
98
+ margin-bottom: 20px;
99
+ flex-wrap: wrap;
100
+ }
101
+ .stat-card {
102
+ background: #161b22;
103
+ border: 1px solid #30363d;
104
+ border-radius: 6px;
105
+ padding: 16px 20px;
106
+ min-width: 150px;
107
+ }
108
+ .stat-card .label {
109
+ color: #8b949e;
110
+ font-size: 12px;
111
+ margin-bottom: 4px;
112
+ }
113
+ .stat-card .value {
114
+ color: #58a6ff;
115
+ font-size: 24px;
116
+ font-weight: 600;
117
+ }
118
+ .message-list {
119
+ display: flex;
120
+ flex-direction: column;
121
+ gap: 12px;
122
+ }
123
+ .message {
124
+ background: #161b22;
125
+ border: 1px solid #30363d;
126
+ border-radius: 8px;
127
+ overflow: hidden;
128
+ }
129
+ .message-header {
130
+ display: flex;
131
+ justify-content: space-between;
132
+ align-items: center;
133
+ padding: 12px 16px;
134
+ background: #21262d;
135
+ border-bottom: 1px solid #30363d;
136
+ cursor: pointer;
137
+ }
138
+ .message-header:hover {
139
+ background: #30363d;
140
+ }
141
+ .message-info {
142
+ display: flex;
143
+ gap: 12px;
144
+ align-items: center;
145
+ flex-wrap: wrap;
146
+ }
147
+ .message-index {
148
+ background: #30363d;
149
+ color: #8b949e;
150
+ padding: 2px 8px;
151
+ border-radius: 4px;
152
+ font-size: 12px;
153
+ font-weight: 500;
154
+ }
155
+ .message-type {
156
+ padding: 2px 8px;
157
+ border-radius: 4px;
158
+ font-size: 12px;
159
+ font-weight: 500;
160
+ }
161
+ .message-type.event {
162
+ background: rgba(56, 139, 253, 0.15);
163
+ color: #58a6ff;
164
+ }
165
+ .message-type.error {
166
+ background: rgba(248, 81, 73, 0.15);
167
+ color: #f85149;
168
+ }
169
+ .message-type.exception {
170
+ background: rgba(210, 153, 34, 0.15);
171
+ color: #d29922;
172
+ }
173
+ .event-type {
174
+ color: #7ee787;
175
+ font-size: 13px;
176
+ }
177
+ .message-size {
178
+ color: #8b949e;
179
+ font-size: 12px;
180
+ }
181
+ .expand-icon {
182
+ color: #8b949e;
183
+ transition: transform 0.2s;
184
+ }
185
+ .message.expanded .expand-icon {
186
+ transform: rotate(90deg);
187
+ }
188
+ .message-content {
189
+ display: none;
190
+ padding: 16px;
191
+ }
192
+ .message.expanded .message-content {
193
+ display: block;
194
+ }
195
+ .section-title {
196
+ color: #8b949e;
197
+ font-size: 12px;
198
+ font-weight: 500;
199
+ margin-bottom: 8px;
200
+ text-transform: uppercase;
201
+ }
202
+ .headers-table {
203
+ width: 100%;
204
+ border-collapse: collapse;
205
+ margin-bottom: 16px;
206
+ font-size: 13px;
207
+ }
208
+ .headers-table th,
209
+ .headers-table td {
210
+ text-align: left;
211
+ padding: 8px 12px;
212
+ border-bottom: 1px solid #21262d;
213
+ }
214
+ .headers-table th {
215
+ color: #8b949e;
216
+ font-weight: 500;
217
+ background: #0d1117;
218
+ }
219
+ .headers-table td {
220
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
221
+ }
222
+ .header-name {
223
+ color: #ff7b72;
224
+ }
225
+ .header-type {
226
+ color: #d2a8ff;
227
+ }
228
+ .header-value {
229
+ color: #a5d6ff;
230
+ }
231
+ .payload-container {
232
+ background: #0d1117;
233
+ border-radius: 6px;
234
+ overflow: hidden;
235
+ }
236
+ .payload-tabs {
237
+ display: flex;
238
+ border-bottom: 1px solid #21262d;
239
+ }
240
+ .payload-tab {
241
+ padding: 8px 16px;
242
+ background: transparent;
243
+ border: none;
244
+ color: #8b949e;
245
+ cursor: pointer;
246
+ font-size: 13px;
247
+ border-bottom: 2px solid transparent;
248
+ margin-bottom: -1px;
249
+ }
250
+ .payload-tab.active {
251
+ color: #58a6ff;
252
+ border-bottom-color: #58a6ff;
253
+ }
254
+ .payload-content {
255
+ padding: 12px;
256
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
257
+ font-size: 13px;
258
+ overflow-x: auto;
259
+ max-height: 400px;
260
+ overflow-y: auto;
261
+ }
262
+ .payload-json {
263
+ white-space: pre-wrap;
264
+ word-break: break-word;
265
+ }
266
+ .payload-raw {
267
+ white-space: pre;
268
+ color: #8b949e;
269
+ }
270
+ .payload-hex {
271
+ white-space: pre;
272
+ color: #8b949e;
273
+ font-size: 12px;
274
+ }
275
+ .json-key {
276
+ color: #ff7b72;
277
+ }
278
+ .json-string {
279
+ color: #a5d6ff;
280
+ }
281
+ .json-number {
282
+ color: #79c0ff;
283
+ }
284
+ .json-boolean {
285
+ color: #ff7b72;
286
+ }
287
+ .json-null {
288
+ color: #8b949e;
289
+ }
290
+ .error-box {
291
+ background: rgba(248, 81, 73, 0.1);
292
+ border: 1px solid rgba(248, 81, 73, 0.4);
293
+ border-radius: 6px;
294
+ padding: 12px 16px;
295
+ color: #f85149;
296
+ margin-bottom: 16px;
297
+ }
298
+ .hex-view {
299
+ display: grid;
300
+ grid-template-columns: 80px 1fr 1fr;
301
+ gap: 8px;
302
+ font-family: 'SF Mono', Monaco, 'Courier New', monospace;
303
+ font-size: 12px;
304
+ }
305
+ .hex-offset {
306
+ color: #8b949e;
307
+ }
308
+ .hex-bytes {
309
+ color: #7ee787;
310
+ }
311
+ .hex-ascii {
312
+ color: #d2a8ff;
313
+ }
314
+ .raw-section {
315
+ margin-top: 20px;
316
+ padding: 16px;
317
+ background: #161b22;
318
+ border: 1px solid #30363d;
319
+ border-radius: 8px;
320
+ }
321
+ .raw-section h3 {
322
+ color: #8b949e;
323
+ font-size: 14px;
324
+ margin-bottom: 12px;
325
+ }
326
+ .copy-btn {
327
+ padding: 4px 8px;
328
+ font-size: 12px;
329
+ margin-left: 8px;
330
+ }
331
+ </style>
332
+ </head>
333
+ <body>
334
+ <div class="container">
335
+ <h1>AWS Event Stream Viewer</h1>
336
+
337
+ <div class="input-section">
338
+ <label>粘贴二进制数据(支持 Hex / Base64 格式)</label>
339
+ <div class="input-controls">
340
+ <select id="inputFormat" class="format-select">
341
+ <option value="auto">自动检测</option>
342
+ <option value="hex">Hex</option>
343
+ <option value="base64">Base64</option>
344
+ </select>
345
+ <button class="btn btn-primary" onclick="parseInput()">解析</button>
346
+ <button class="btn btn-secondary" onclick="clearAll()">清空</button>
347
+ <button class="btn btn-secondary" onclick="loadExample()">加载示例</button>
348
+ </div>
349
+ <textarea id="inputData" placeholder="粘贴 Hex 数据 (如: 00 00 00 3e 00 00 00 1d...) 或 Base64 编码数据..."></textarea>
350
+ </div>
351
+
352
+ <div id="error" class="error-box" style="display: none;"></div>
353
+
354
+ <div class="results" id="results" style="display: none;">
355
+ <div class="stats" id="stats"></div>
356
+ <div class="message-list" id="messageList"></div>
357
+ </div>
358
+
359
+ <div class="raw-section" id="rawSection" style="display: none;">
360
+ <h3>原始字节数据 <button class="btn btn-secondary copy-btn" onclick="copyRawHex()">复制 Hex</button></h3>
361
+ <div class="payload-hex" id="rawHex"></div>
362
+ </div>
363
+ </div>
364
+
365
+ <script>
366
+ // CRC32 (IEEE/ISO-HDLC) 实现
367
+ const CRC32_TABLE = (() => {
368
+ const table = new Uint32Array(256);
369
+ for (let i = 0; i < 256; i++) {
370
+ let crc = i;
371
+ for (let j = 0; j < 8; j++) {
372
+ crc = (crc & 1) ? (0xEDB88320 ^ (crc >>> 1)) : (crc >>> 1);
373
+ }
374
+ table[i] = crc >>> 0;
375
+ }
376
+ return table;
377
+ })();
378
+
379
+ function crc32(data) {
380
+ let crc = 0xFFFFFFFF;
381
+ for (let i = 0; i < data.length; i++) {
382
+ crc = CRC32_TABLE[(crc ^ data[i]) & 0xFF] ^ (crc >>> 8);
383
+ }
384
+ return (crc ^ 0xFFFFFFFF) >>> 0;
385
+ }
386
+
387
+ // 值类型定义
388
+ const VALUE_TYPES = {
389
+ 0: { name: 'BoolTrue', size: 0 },
390
+ 1: { name: 'BoolFalse', size: 0 },
391
+ 2: { name: 'Byte', size: 1 },
392
+ 3: { name: 'Short', size: 2 },
393
+ 4: { name: 'Integer', size: 4 },
394
+ 5: { name: 'Long', size: 8 },
395
+ 6: { name: 'ByteArray', size: -1 },
396
+ 7: { name: 'String', size: -1 },
397
+ 8: { name: 'Timestamp', size: 8 },
398
+ 9: { name: 'UUID', size: 16 }
399
+ };
400
+
401
+ // 输入格式检测
402
+ function detectFormat(input) {
403
+ const cleaned = input.replace(/[\s\n\r]/g, '');
404
+
405
+ // 检测是否是Base64
406
+ if (/^[A-Za-z0-9+/]+=*$/.test(cleaned) && cleaned.length % 4 === 0) {
407
+ // 尝试解码验证
408
+ try {
409
+ atob(cleaned);
410
+ return 'base64';
411
+ } catch {}
412
+ }
413
+
414
+ // 检测是否是Hex
415
+ if (/^[0-9A-Fa-f\s]+$/.test(input)) {
416
+ return 'hex';
417
+ }
418
+
419
+ return 'unknown';
420
+ }
421
+
422
+ // 解析输入数据为字节数组
423
+ function parseInputData(input, format) {
424
+ const cleaned = input.trim();
425
+
426
+ if (format === 'auto') {
427
+ format = detectFormat(cleaned);
428
+ }
429
+
430
+ if (format === 'base64') {
431
+ try {
432
+ const binary = atob(cleaned.replace(/[\s\n\r]/g, ''));
433
+ return new Uint8Array([...binary].map(c => c.charCodeAt(0)));
434
+ } catch (e) {
435
+ throw new Error('Base64 解码失败: ' + e.message);
436
+ }
437
+ }
438
+
439
+ if (format === 'hex') {
440
+ const hex = cleaned.replace(/[\s\n\r]/g, '').replace(/0x/gi, '');
441
+ if (hex.length % 2 !== 0) {
442
+ throw new Error('Hex 数据长度必须是偶数');
443
+ }
444
+ const bytes = new Uint8Array(hex.length / 2);
445
+ for (let i = 0; i < hex.length; i += 2) {
446
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
447
+ }
448
+ return bytes;
449
+ }
450
+
451
+ throw new Error('无法识别输入格式');
452
+ }
453
+
454
+ // 读取大端序整数
455
+ function readUint32BE(data, offset) {
456
+ return (data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3]) >>> 0;
457
+ }
458
+
459
+ function readUint16BE(data, offset) {
460
+ return (data[offset] << 8 | data[offset + 1]) >>> 0;
461
+ }
462
+
463
+ function readInt64BE(data, offset) {
464
+ // JavaScript BigInt for 64-bit
465
+ let high = readUint32BE(data, offset);
466
+ let low = readUint32BE(data, offset + 4);
467
+ return BigInt(high) << 32n | BigInt(low);
468
+ }
469
+
470
+ // 解析头部
471
+ function parseHeaders(data, headerLength) {
472
+ const headers = [];
473
+ let offset = 0;
474
+
475
+ while (offset < headerLength) {
476
+ // 读取名称长度
477
+ const nameLength = data[offset];
478
+ offset++;
479
+
480
+ if (nameLength === 0 || offset + nameLength > headerLength) {
481
+ break;
482
+ }
483
+
484
+ // 读取名称
485
+ const name = new TextDecoder().decode(data.slice(offset, offset + nameLength));
486
+ offset += nameLength;
487
+
488
+ if (offset >= headerLength) break;
489
+
490
+ // 读取值类型
491
+ const valueType = data[offset];
492
+ offset++;
493
+
494
+ const typeInfo = VALUE_TYPES[valueType] || { name: 'Unknown', size: 0 };
495
+ let value;
496
+ let valueSize = typeInfo.size;
497
+
498
+ // 解析值
499
+ switch (valueType) {
500
+ case 0: // BoolTrue
501
+ value = true;
502
+ break;
503
+ case 1: // BoolFalse
504
+ value = false;
505
+ break;
506
+ case 2: // Byte
507
+ value = data[offset];
508
+ offset++;
509
+ break;
510
+ case 3: // Short
511
+ value = readUint16BE(data, offset);
512
+ offset += 2;
513
+ break;
514
+ case 4: // Integer
515
+ value = readUint32BE(data, offset);
516
+ offset += 4;
517
+ break;
518
+ case 5: // Long
519
+ value = readInt64BE(data, offset).toString();
520
+ offset += 8;
521
+ break;
522
+ case 6: // ByteArray
523
+ valueSize = readUint16BE(data, offset);
524
+ offset += 2;
525
+ value = Array.from(data.slice(offset, offset + valueSize)).map(b => b.toString(16).padStart(2, '0')).join(' ');
526
+ offset += valueSize;
527
+ break;
528
+ case 7: // String
529
+ valueSize = readUint16BE(data, offset);
530
+ offset += 2;
531
+ value = new TextDecoder().decode(data.slice(offset, offset + valueSize));
532
+ offset += valueSize;
533
+ break;
534
+ case 8: // Timestamp
535
+ value = new Date(Number(readInt64BE(data, offset))).toISOString();
536
+ offset += 8;
537
+ break;
538
+ case 9: // UUID
539
+ const uuidBytes = data.slice(offset, offset + 16);
540
+ value = Array.from(uuidBytes).map(b => b.toString(16).padStart(2, '0')).join('');
541
+ value = `${value.slice(0,8)}-${value.slice(8,12)}-${value.slice(12,16)}-${value.slice(16,20)}-${value.slice(20)}`;
542
+ offset += 16;
543
+ break;
544
+ default:
545
+ value = '(unknown type)';
546
+ }
547
+
548
+ headers.push({ name, type: typeInfo.name, typeCode: valueType, value });
549
+ }
550
+
551
+ return headers;
552
+ }
553
+
554
+ // 解析单个消息帧
555
+ function parseFrame(data, offset) {
556
+ if (data.length - offset < 16) {
557
+ return null;
558
+ }
559
+
560
+ const totalLength = readUint32BE(data, offset);
561
+ const headerLength = readUint32BE(data, offset + 4);
562
+ const preludeCrc = readUint32BE(data, offset + 8);
563
+
564
+ if (totalLength < 16 || totalLength > 16 * 1024 * 1024) {
565
+ throw new Error(`消息长度异常: ${totalLength}`);
566
+ }
567
+
568
+ if (data.length - offset < totalLength) {
569
+ return null;
570
+ }
571
+
572
+ // 验证 Prelude CRC
573
+ const actualPreludeCrc = crc32(data.slice(offset, offset + 8));
574
+ const preludeCrcValid = preludeCrc === actualPreludeCrc;
575
+
576
+ // 验证 Message CRC
577
+ const messageCrc = readUint32BE(data, offset + totalLength - 4);
578
+ const actualMessageCrc = crc32(data.slice(offset, offset + totalLength - 4));
579
+ const messageCrcValid = messageCrc === actualMessageCrc;
580
+
581
+ // 解析头部
582
+ const headersStart = offset + 12;
583
+ const headersEnd = headersStart + headerLength;
584
+ const headers = parseHeaders(data.slice(headersStart, headersEnd), headerLength);
585
+
586
+ // 提取 payload
587
+ const payloadStart = headersEnd;
588
+ const payloadEnd = offset + totalLength - 4;
589
+ const payload = data.slice(payloadStart, payloadEnd);
590
+
591
+ // 获取消息类型
592
+ const messageType = headers.find(h => h.name === ':message-type')?.value || 'event';
593
+ const eventType = headers.find(h => h.name === ':event-type')?.value || '';
594
+ const contentType = headers.find(h => h.name === ':content-type')?.value || 'application/json';
595
+
596
+ return {
597
+ totalLength,
598
+ headerLength,
599
+ preludeCrc: { expected: preludeCrc, actual: actualPreludeCrc, valid: preludeCrcValid },
600
+ messageCrc: { expected: messageCrc, actual: actualMessageCrc, valid: messageCrcValid },
601
+ headers,
602
+ payload,
603
+ messageType,
604
+ eventType,
605
+ contentType,
606
+ rawBytes: data.slice(offset, offset + totalLength)
607
+ };
608
+ }
609
+
610
+ // 解析所有消息
611
+ function parseAllMessages(data) {
612
+ const messages = [];
613
+ let offset = 0;
614
+
615
+ while (offset < data.length) {
616
+ try {
617
+ const frame = parseFrame(data, offset);
618
+ if (!frame) {
619
+ break;
620
+ }
621
+ messages.push(frame);
622
+ offset += frame.totalLength;
623
+ } catch (e) {
624
+ console.error('Parse error at offset', offset, e);
625
+ // 尝试跳过一个字节继续
626
+ offset++;
627
+ }
628
+ }
629
+
630
+ return messages;
631
+ }
632
+
633
+ // 格式化 JSON 带语法高亮
634
+ function formatJson(obj, indent = 0) {
635
+ const spaces = ' '.repeat(indent);
636
+
637
+ if (obj === null) {
638
+ return '<span class="json-null">null</span>';
639
+ }
640
+
641
+ if (typeof obj === 'boolean') {
642
+ return `<span class="json-boolean">${obj}</span>`;
643
+ }
644
+
645
+ if (typeof obj === 'number') {
646
+ return `<span class="json-number">${obj}</span>`;
647
+ }
648
+
649
+ if (typeof obj === 'string') {
650
+ const escaped = obj.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
651
+ return `<span class="json-string">"${escaped}"</span>`;
652
+ }
653
+
654
+ if (Array.isArray(obj)) {
655
+ if (obj.length === 0) return '[]';
656
+ const items = obj.map(item => spaces + ' ' + formatJson(item, indent + 1));
657
+ return '[\n' + items.join(',\n') + '\n' + spaces + ']';
658
+ }
659
+
660
+ if (typeof obj === 'object') {
661
+ const keys = Object.keys(obj);
662
+ if (keys.length === 0) return '{}';
663
+ const items = keys.map(key => {
664
+ const escapedKey = key.replace(/"/g, '\\"');
665
+ return spaces + ' ' + `<span class="json-key">"${escapedKey}"</span>: ` + formatJson(obj[key], indent + 1);
666
+ });
667
+ return '{\n' + items.join(',\n') + '\n' + spaces + '}';
668
+ }
669
+
670
+ return String(obj);
671
+ }
672
+
673
+ // 格式化 Hex 视图
674
+ function formatHexView(data) {
675
+ const lines = [];
676
+ for (let i = 0; i < data.length; i += 16) {
677
+ const offset = i.toString(16).padStart(8, '0');
678
+ const bytes = [];
679
+ const ascii = [];
680
+
681
+ for (let j = 0; j < 16; j++) {
682
+ if (i + j < data.length) {
683
+ bytes.push(data[i + j].toString(16).padStart(2, '0'));
684
+ const char = data[i + j];
685
+ ascii.push(char >= 32 && char <= 126 ? String.fromCharCode(char) : '.');
686
+ } else {
687
+ bytes.push(' ');
688
+ ascii.push(' ');
689
+ }
690
+ }
691
+
692
+ lines.push(`<span class="hex-offset">${offset}</span> <span class="hex-bytes">${bytes.join(' ')}</span> <span class="hex-ascii">${ascii.join('')}</span>`);
693
+ }
694
+ return lines.join('\n');
695
+ }
696
+
697
+ // 渲染单个消息
698
+ function renderMessage(message, index) {
699
+ const messageTypeClass = message.messageType === 'error' ? 'error' :
700
+ message.messageType === 'exception' ? 'exception' : 'event';
701
+
702
+ let payloadText = '';
703
+ let payloadJson = null;
704
+
705
+ try {
706
+ payloadText = new TextDecoder().decode(message.payload);
707
+ try {
708
+ payloadJson = JSON.parse(payloadText);
709
+ } catch {}
710
+ } catch {}
711
+
712
+ const headersHtml = message.headers.map(h => `
713
+ <tr>
714
+ <td><span class="header-name">${h.name}</span></td>
715
+ <td><span class="header-type">${h.type}</span></td>
716
+ <td><span class="header-value">${typeof h.value === 'string' ? h.value.replace(/</g, '&lt;').replace(/>/g, '&gt;') : h.value}</span></td>
717
+ </tr>
718
+ `).join('');
719
+
720
+ const crcStatus = (crc) => crc.valid
721
+ ? '<span style="color: #7ee787;">&#10003;</span>'
722
+ : `<span style="color: #f85149;">&#10007; (expected: ${crc.expected.toString(16)}, got: ${crc.actual.toString(16)})</span>`;
723
+
724
+ return `
725
+ <div class="message" id="message-${index}">
726
+ <div class="message-header" onclick="toggleMessage(${index})">
727
+ <div class="message-info">
728
+ <span class="message-index">#${index + 1}</span>
729
+ <span class="message-type ${messageTypeClass}">${message.messageType}</span>
730
+ ${message.eventType ? `<span class="event-type">${message.eventType}</span>` : ''}
731
+ </div>
732
+ <div style="display: flex; align-items: center; gap: 12px;">
733
+ <span class="message-size">${message.totalLength} bytes</span>
734
+ <span class="expand-icon">&#9654;</span>
735
+ </div>
736
+ </div>
737
+ <div class="message-content">
738
+ <div class="section-title">CRC 校验</div>
739
+ <table class="headers-table" style="margin-bottom: 16px;">
740
+ <tr><td>Prelude CRC</td><td>${crcStatus(message.preludeCrc)}</td></tr>
741
+ <tr><td>Message CRC</td><td>${crcStatus(message.messageCrc)}</td></tr>
742
+ </table>
743
+
744
+ <div class="section-title">Headers (${message.headers.length})</div>
745
+ <table class="headers-table">
746
+ <thead>
747
+ <tr><th>Name</th><th>Type</th><th>Value</th></tr>
748
+ </thead>
749
+ <tbody>
750
+ ${headersHtml}
751
+ </tbody>
752
+ </table>
753
+
754
+ <div class="section-title">Payload (${message.payload.length} bytes)</div>
755
+ <div class="payload-container">
756
+ <div class="payload-tabs">
757
+ <button class="payload-tab active" onclick="switchPayloadTab(${index}, 'json')">JSON</button>
758
+ <button class="payload-tab" onclick="switchPayloadTab(${index}, 'raw')">Raw</button>
759
+ <button class="payload-tab" onclick="switchPayloadTab(${index}, 'hex')">Hex</button>
760
+ </div>
761
+ <div class="payload-content">
762
+ <div class="payload-json" id="payload-json-${index}">${payloadJson ? formatJson(payloadJson) : `<span style="color: #8b949e;">${payloadText.replace(/</g, '&lt;').replace(/>/g, '&gt;') || '(empty)'}</span>`}</div>
763
+ <div class="payload-raw" id="payload-raw-${index}" style="display: none;">${payloadText.replace(/</g, '&lt;').replace(/>/g, '&gt;') || '(empty)'}</div>
764
+ <div class="payload-hex" id="payload-hex-${index}" style="display: none;">${formatHexView(message.payload)}</div>
765
+ </div>
766
+ </div>
767
+ </div>
768
+ </div>
769
+ `;
770
+ }
771
+
772
+ let parsedData = null;
773
+
774
+ function parseInput() {
775
+ const input = document.getElementById('inputData').value;
776
+ const format = document.getElementById('inputFormat').value;
777
+ const errorBox = document.getElementById('error');
778
+ const resultsBox = document.getElementById('results');
779
+ const rawSection = document.getElementById('rawSection');
780
+
781
+ errorBox.style.display = 'none';
782
+ resultsBox.style.display = 'none';
783
+ rawSection.style.display = 'none';
784
+
785
+ if (!input.trim()) {
786
+ errorBox.textContent = '请输入数据';
787
+ errorBox.style.display = 'block';
788
+ return;
789
+ }
790
+
791
+ try {
792
+ const data = parseInputData(input, format);
793
+ parsedData = data;
794
+
795
+ // 显示原始 Hex
796
+ document.getElementById('rawHex').innerHTML = formatHexView(data);
797
+ rawSection.style.display = 'block';
798
+
799
+ const messages = parseAllMessages(data);
800
+
801
+ if (messages.length === 0) {
802
+ errorBox.textContent = '未能解析出任何消息。请检查输入数据格式。';
803
+ errorBox.style.display = 'block';
804
+ return;
805
+ }
806
+
807
+ // 统计
808
+ const eventTypes = {};
809
+ const messageTypes = {};
810
+ messages.forEach(m => {
811
+ messageTypes[m.messageType] = (messageTypes[m.messageType] || 0) + 1;
812
+ if (m.eventType) {
813
+ eventTypes[m.eventType] = (eventTypes[m.eventType] || 0) + 1;
814
+ }
815
+ });
816
+
817
+ document.getElementById('stats').innerHTML = `
818
+ <div class="stat-card">
819
+ <div class="label">总消息数</div>
820
+ <div class="value">${messages.length}</div>
821
+ </div>
822
+ <div class="stat-card">
823
+ <div class="label">总字节数</div>
824
+ <div class="value">${data.length}</div>
825
+ </div>
826
+ <div class="stat-card">
827
+ <div class="label">消息类型</div>
828
+ <div class="value" style="font-size: 14px;">${Object.entries(messageTypes).map(([k, v]) => `${k}: ${v}`).join(', ')}</div>
829
+ </div>
830
+ <div class="stat-card">
831
+ <div class="label">事件类型</div>
832
+ <div class="value" style="font-size: 14px;">${Object.entries(eventTypes).map(([k, v]) => `${k}: ${v}`).join(', ') || '-'}</div>
833
+ </div>
834
+ `;
835
+
836
+ document.getElementById('messageList').innerHTML = messages.map((m, i) => renderMessage(m, i)).join('');
837
+ resultsBox.style.display = 'block';
838
+
839
+ } catch (e) {
840
+ errorBox.textContent = '解析错误: ' + e.message;
841
+ errorBox.style.display = 'block';
842
+ }
843
+ }
844
+
845
+ function toggleMessage(index) {
846
+ const msg = document.getElementById(`message-${index}`);
847
+ msg.classList.toggle('expanded');
848
+ }
849
+
850
+ function switchPayloadTab(index, tab) {
851
+ const tabs = document.querySelectorAll(`#message-${index} .payload-tab`);
852
+ tabs.forEach(t => t.classList.remove('active'));
853
+ event.target.classList.add('active');
854
+
855
+ document.getElementById(`payload-json-${index}`).style.display = tab === 'json' ? 'block' : 'none';
856
+ document.getElementById(`payload-raw-${index}`).style.display = tab === 'raw' ? 'block' : 'none';
857
+ document.getElementById(`payload-hex-${index}`).style.display = tab === 'hex' ? 'block' : 'none';
858
+ }
859
+
860
+ function clearAll() {
861
+ document.getElementById('inputData').value = '';
862
+ document.getElementById('error').style.display = 'none';
863
+ document.getElementById('results').style.display = 'none';
864
+ document.getElementById('rawSection').style.display = 'none';
865
+ parsedData = null;
866
+ }
867
+
868
+ function copyRawHex() {
869
+ if (!parsedData) return;
870
+ const hex = Array.from(parsedData).map(b => b.toString(16).padStart(2, '0')).join(' ');
871
+ navigator.clipboard.writeText(hex).then(() => {
872
+ alert('已复制到剪贴板');
873
+ });
874
+ }
875
+
876
+ function loadExample() {
877
+ // 构造一个示例事件流消息
878
+ // 这是一个简单的 assistantResponseEvent 消息
879
+ const example = `
880
+ 00 00 00 8f 00 00 00 47 7d 83 6e 75 0d 3a 6d 65
881
+ 73 73 61 67 65 2d 74 79 70 65 07 00 05 65 76 65
882
+ 6e 74 0b 3a 65 76 65 6e 74 2d 74 79 70 65 07 00
883
+ 16 61 73 73 69 73 74 61 6e 74 52 65 73 70 6f 6e
884
+ 73 65 45 76 65 6e 74 0d 3a 63 6f 6e 74 65 6e 74
885
+ 2d 74 79 70 65 07 00 10 61 70 70 6c 69 63 61 74
886
+ 69 6f 6e 2f 6a 73 6f 6e 7b 22 63 6f 6e 74 65 6e
887
+ 74 22 3a 22 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64
888
+ 21 22 2c 22 73 74 6f 70 22 3a 66 61 6c 73 65 7d
889
+ c7 8d c2 bc
890
+ `.trim();
891
+ document.getElementById('inputData').value = example;
892
+ document.getElementById('inputFormat').value = 'hex';
893
+ }
894
+ </script>
895
+ </body>
896
+ </html>