@zhongqian97-code/ecode 0.5.19 → 0.5.21
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.
- package/dist/index.js +877 -93
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5255,119 +5255,866 @@ function generateAdminHtml(version2) {
|
|
|
5255
5255
|
<style>
|
|
5256
5256
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
5257
5257
|
body {
|
|
5258
|
-
font-family:
|
|
5259
|
-
background: #0d1117;
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
display: flex;
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5258
|
+
font-family: "Courier New", Courier, monospace;
|
|
5259
|
+
background: #0d1117;
|
|
5260
|
+
color: #c9d1d9;
|
|
5261
|
+
height: 100vh;
|
|
5262
|
+
display: flex;
|
|
5263
|
+
flex-direction: column;
|
|
5264
|
+
overflow: hidden;
|
|
5265
|
+
}
|
|
5266
|
+
/* \u2500\u2500 Top bar \u2500\u2500 */
|
|
5267
|
+
#topbar {
|
|
5268
|
+
display: flex;
|
|
5269
|
+
align-items: center;
|
|
5270
|
+
gap: 10px;
|
|
5271
|
+
padding: 8px 14px;
|
|
5272
|
+
background: #161b22;
|
|
5273
|
+
border-bottom: 1px solid #30363d;
|
|
5274
|
+
flex-shrink: 0;
|
|
5275
|
+
}
|
|
5276
|
+
#topbar h1 { font-size: 14px; color: #e6edf3; font-weight: 600; }
|
|
5277
|
+
#topbar .version { font-size: 11px; color: #8b949e; background: #21262d;
|
|
5278
|
+
padding: 1px 7px; border-radius: 20px; }
|
|
5279
|
+
#hamburger {
|
|
5280
|
+
display: none; background: none; border: none; color: #c9d1d9;
|
|
5281
|
+
font-size: 18px; cursor: pointer; padding: 2px 6px;
|
|
5282
|
+
}
|
|
5283
|
+
|
|
5284
|
+
/* \u2500\u2500 Main layout \u2500\u2500 */
|
|
5285
|
+
#app {
|
|
5286
|
+
display: flex;
|
|
5287
|
+
flex: 1;
|
|
5288
|
+
overflow: hidden;
|
|
5289
|
+
}
|
|
5290
|
+
|
|
5291
|
+
/* \u2500\u2500 Sidebar \u2500\u2500 */
|
|
5292
|
+
#sidebar {
|
|
5293
|
+
width: 220px;
|
|
5294
|
+
flex-shrink: 0;
|
|
5295
|
+
background: #161b22;
|
|
5296
|
+
border-right: 1px solid #30363d;
|
|
5297
|
+
display: flex;
|
|
5298
|
+
flex-direction: column;
|
|
5299
|
+
overflow: hidden;
|
|
5300
|
+
}
|
|
5301
|
+
#sidebar-header {
|
|
5302
|
+
padding: 10px 12px;
|
|
5303
|
+
border-bottom: 1px solid #30363d;
|
|
5304
|
+
flex-shrink: 0;
|
|
5305
|
+
}
|
|
5306
|
+
#new-session-btn {
|
|
5307
|
+
width: 100%;
|
|
5308
|
+
padding: 6px 10px;
|
|
5309
|
+
background: #21262d;
|
|
5310
|
+
color: #79c0ff;
|
|
5311
|
+
border: 1px solid #30363d;
|
|
5312
|
+
border-radius: 4px;
|
|
5313
|
+
cursor: pointer;
|
|
5314
|
+
font-family: inherit;
|
|
5315
|
+
font-size: 12px;
|
|
5316
|
+
text-align: left;
|
|
5317
|
+
}
|
|
5318
|
+
#new-session-btn:hover { background: #30363d; }
|
|
5319
|
+
#session-list {
|
|
5320
|
+
flex: 1;
|
|
5321
|
+
overflow-y: auto;
|
|
5322
|
+
padding: 6px 0;
|
|
5323
|
+
}
|
|
5283
5324
|
.session-item {
|
|
5284
|
-
padding: 8px
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
.
|
|
5291
|
-
.
|
|
5292
|
-
|
|
5293
|
-
border-
|
|
5294
|
-
|
|
5295
|
-
.
|
|
5296
|
-
.
|
|
5325
|
+
padding: 8px 12px;
|
|
5326
|
+
cursor: pointer;
|
|
5327
|
+
border-left: 2px solid transparent;
|
|
5328
|
+
font-size: 12px;
|
|
5329
|
+
line-height: 1.4;
|
|
5330
|
+
}
|
|
5331
|
+
.session-item:hover { background: #21262d; }
|
|
5332
|
+
.session-item.active {
|
|
5333
|
+
background: #21262d;
|
|
5334
|
+
border-left-color: #79c0ff;
|
|
5335
|
+
}
|
|
5336
|
+
.session-item .s-title { color: #c9d1d9; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
5337
|
+
.session-item .s-id { color: #8b949e; font-size: 10px; }
|
|
5338
|
+
.sidebar-empty { color: #8b949e; font-size: 12px; font-style: italic; padding: 12px; }
|
|
5339
|
+
|
|
5340
|
+
/* \u2500\u2500 Chat area \u2500\u2500 */
|
|
5341
|
+
#chat-area {
|
|
5342
|
+
flex: 1;
|
|
5343
|
+
display: flex;
|
|
5344
|
+
flex-direction: column;
|
|
5345
|
+
overflow: hidden;
|
|
5346
|
+
}
|
|
5347
|
+
#chat-header {
|
|
5348
|
+
padding: 10px 14px;
|
|
5349
|
+
border-bottom: 1px solid #30363d;
|
|
5350
|
+
background: #161b22;
|
|
5351
|
+
flex-shrink: 0;
|
|
5352
|
+
display: flex;
|
|
5353
|
+
align-items: center;
|
|
5354
|
+
gap: 10px;
|
|
5355
|
+
}
|
|
5356
|
+
#chat-title { font-size: 13px; color: #8b949e; }
|
|
5357
|
+
#status-indicator {
|
|
5358
|
+
margin-left: auto;
|
|
5359
|
+
font-size: 11px;
|
|
5360
|
+
color: #8b949e;
|
|
5361
|
+
display: flex;
|
|
5362
|
+
align-items: center;
|
|
5363
|
+
gap: 5px;
|
|
5364
|
+
}
|
|
5365
|
+
#status-dot {
|
|
5366
|
+
width: 7px; height: 7px; border-radius: 50%;
|
|
5367
|
+
background: #3fb950;
|
|
5368
|
+
}
|
|
5369
|
+
#status-dot.thinking { background: #d29922; animation: pulse 1s infinite; }
|
|
5370
|
+
#status-dot.error { background: #f85149; }
|
|
5371
|
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.3} }
|
|
5372
|
+
|
|
5373
|
+
/* \u2500\u2500 Messages \u2500\u2500 */
|
|
5374
|
+
#messages {
|
|
5375
|
+
flex: 1;
|
|
5376
|
+
overflow-y: auto;
|
|
5377
|
+
padding: 14px;
|
|
5378
|
+
display: flex;
|
|
5379
|
+
flex-direction: column;
|
|
5380
|
+
gap: 10px;
|
|
5381
|
+
}
|
|
5382
|
+
.msg {
|
|
5383
|
+
font-size: 13px;
|
|
5384
|
+
line-height: 1.6;
|
|
5385
|
+
max-width: 100%;
|
|
5386
|
+
}
|
|
5387
|
+
.msg-header {
|
|
5388
|
+
font-size: 11px;
|
|
5389
|
+
margin-bottom: 3px;
|
|
5390
|
+
font-weight: 600;
|
|
5391
|
+
}
|
|
5392
|
+
.msg-header.user { color: #3fb950; }
|
|
5393
|
+
.msg-header.assistant { color: #c9d1d9; }
|
|
5394
|
+
.msg-header.tool { color: #8b949e; }
|
|
5395
|
+
.msg-body { color: #c9d1d9; white-space: pre-wrap; word-break: break-word; }
|
|
5396
|
+
.msg-body code {
|
|
5397
|
+
background: #21262d;
|
|
5398
|
+
padding: 1px 5px;
|
|
5399
|
+
border-radius: 3px;
|
|
5400
|
+
font-family: inherit;
|
|
5401
|
+
font-size: 12px;
|
|
5402
|
+
}
|
|
5403
|
+
.msg-body pre {
|
|
5404
|
+
background: #161b22;
|
|
5405
|
+
border: 1px solid #30363d;
|
|
5406
|
+
border-radius: 4px;
|
|
5407
|
+
padding: 10px;
|
|
5408
|
+
overflow-x: auto;
|
|
5409
|
+
margin: 6px 0;
|
|
5410
|
+
}
|
|
5411
|
+
.msg-body pre code { background: none; padding: 0; }
|
|
5412
|
+
.msg-body strong { color: #e6edf3; }
|
|
5413
|
+
.tool-box {
|
|
5414
|
+
border: 1px solid #30363d;
|
|
5415
|
+
border-radius: 4px;
|
|
5416
|
+
font-size: 12px;
|
|
5417
|
+
overflow: hidden;
|
|
5418
|
+
margin: 4px 0;
|
|
5419
|
+
}
|
|
5420
|
+
.tool-box-header {
|
|
5421
|
+
background: #21262d;
|
|
5422
|
+
padding: 4px 10px;
|
|
5423
|
+
color: #8b949e;
|
|
5424
|
+
font-size: 11px;
|
|
5425
|
+
}
|
|
5426
|
+
.tool-box-body {
|
|
5427
|
+
padding: 8px 10px;
|
|
5428
|
+
color: #c9d1d9;
|
|
5429
|
+
white-space: pre-wrap;
|
|
5430
|
+
word-break: break-word;
|
|
5431
|
+
}
|
|
5432
|
+
.cursor-blink::after {
|
|
5433
|
+
content: "\u258B";
|
|
5434
|
+
animation: blink 1s step-start infinite;
|
|
5435
|
+
}
|
|
5436
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
5437
|
+
.empty-chat {
|
|
5438
|
+
color: #8b949e;
|
|
5439
|
+
font-size: 13px;
|
|
5440
|
+
font-style: italic;
|
|
5441
|
+
text-align: center;
|
|
5442
|
+
padding: 40px 20px;
|
|
5443
|
+
}
|
|
5444
|
+
.ws-status {
|
|
5445
|
+
font-size: 11px;
|
|
5446
|
+
color: #8b949e;
|
|
5447
|
+
text-align: center;
|
|
5448
|
+
padding: 4px;
|
|
5449
|
+
flex-shrink: 0;
|
|
5450
|
+
}
|
|
5451
|
+
|
|
5452
|
+
/* \u2500\u2500 Input bar \u2500\u2500 */
|
|
5453
|
+
#input-bar {
|
|
5454
|
+
border-top: 1px solid #30363d;
|
|
5455
|
+
background: #161b22;
|
|
5456
|
+
padding: 10px 14px;
|
|
5457
|
+
display: flex;
|
|
5458
|
+
gap: 8px;
|
|
5459
|
+
align-items: flex-end;
|
|
5460
|
+
flex-shrink: 0;
|
|
5461
|
+
}
|
|
5462
|
+
#msg-input {
|
|
5463
|
+
flex: 1;
|
|
5464
|
+
background: #0d1117;
|
|
5465
|
+
border: 1px solid #30363d;
|
|
5466
|
+
border-radius: 4px;
|
|
5467
|
+
color: #c9d1d9;
|
|
5468
|
+
font-family: inherit;
|
|
5469
|
+
font-size: 13px;
|
|
5470
|
+
padding: 8px 10px;
|
|
5471
|
+
resize: none;
|
|
5472
|
+
outline: none;
|
|
5473
|
+
min-height: 38px;
|
|
5474
|
+
max-height: 120px;
|
|
5475
|
+
}
|
|
5476
|
+
#msg-input:focus { border-color: #58a6ff; }
|
|
5477
|
+
#msg-input:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
5478
|
+
#send-btn {
|
|
5479
|
+
padding: 8px 14px;
|
|
5480
|
+
background: #21262d;
|
|
5481
|
+
color: #79c0ff;
|
|
5482
|
+
border: 1px solid #30363d;
|
|
5483
|
+
border-radius: 4px;
|
|
5484
|
+
cursor: pointer;
|
|
5485
|
+
font-family: inherit;
|
|
5486
|
+
font-size: 13px;
|
|
5487
|
+
white-space: nowrap;
|
|
5488
|
+
flex-shrink: 0;
|
|
5489
|
+
}
|
|
5490
|
+
#send-btn:hover:not(:disabled) { background: #30363d; }
|
|
5491
|
+
#send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
5492
|
+
|
|
5493
|
+
/* \u2500\u2500 Approval modal \u2500\u2500 */
|
|
5494
|
+
#approval-modal {
|
|
5495
|
+
display: none;
|
|
5496
|
+
position: fixed;
|
|
5497
|
+
inset: 0;
|
|
5498
|
+
background: rgba(0,0,0,.65);
|
|
5499
|
+
z-index: 100;
|
|
5500
|
+
align-items: center;
|
|
5501
|
+
justify-content: center;
|
|
5502
|
+
}
|
|
5503
|
+
#approval-modal.visible { display: flex; }
|
|
5504
|
+
#approval-box {
|
|
5505
|
+
background: #161b22;
|
|
5506
|
+
border: 1px solid #30363d;
|
|
5507
|
+
border-radius: 6px;
|
|
5508
|
+
width: min(520px, 90vw);
|
|
5509
|
+
padding: 20px;
|
|
5510
|
+
}
|
|
5511
|
+
#approval-box.danger { border-color: #f85149; }
|
|
5512
|
+
#approval-title {
|
|
5513
|
+
font-size: 13px;
|
|
5514
|
+
font-weight: 600;
|
|
5515
|
+
color: #e6edf3;
|
|
5516
|
+
margin-bottom: 10px;
|
|
5517
|
+
}
|
|
5518
|
+
#approval-prompt {
|
|
5519
|
+
background: #0d1117;
|
|
5520
|
+
border: 1px solid #30363d;
|
|
5521
|
+
border-radius: 4px;
|
|
5522
|
+
padding: 10px;
|
|
5523
|
+
font-size: 12px;
|
|
5524
|
+
color: #c9d1d9;
|
|
5525
|
+
white-space: pre-wrap;
|
|
5526
|
+
word-break: break-word;
|
|
5527
|
+
max-height: 200px;
|
|
5528
|
+
overflow-y: auto;
|
|
5529
|
+
margin-bottom: 14px;
|
|
5530
|
+
}
|
|
5531
|
+
#approval-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
|
5532
|
+
#deny-btn {
|
|
5533
|
+
padding: 6px 14px;
|
|
5534
|
+
background: none;
|
|
5535
|
+
border: 1px solid #30363d;
|
|
5536
|
+
color: #c9d1d9;
|
|
5537
|
+
border-radius: 4px;
|
|
5538
|
+
cursor: pointer;
|
|
5539
|
+
font-family: inherit;
|
|
5540
|
+
font-size: 13px;
|
|
5541
|
+
}
|
|
5542
|
+
#deny-btn:hover { background: #21262d; }
|
|
5543
|
+
#approve-btn {
|
|
5544
|
+
padding: 6px 14px;
|
|
5545
|
+
background: #1a4731;
|
|
5546
|
+
border: 1px solid #3fb950;
|
|
5547
|
+
color: #3fb950;
|
|
5548
|
+
border-radius: 4px;
|
|
5549
|
+
cursor: pointer;
|
|
5550
|
+
font-family: inherit;
|
|
5551
|
+
font-size: 13px;
|
|
5552
|
+
}
|
|
5553
|
+
#approve-btn:hover { background: #2ea043; color: #fff; }
|
|
5554
|
+
|
|
5555
|
+
/* \u2500\u2500 Config & upgrade modals \u2500\u2500 */
|
|
5556
|
+
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,.7); z-index:100; align-items:center; justify-content:center; }
|
|
5557
|
+
.modal-overlay.open { display:flex; }
|
|
5558
|
+
.modal { background:#161b22; border:1px solid #30363d; border-radius:8px; padding:24px; width:min(480px,90vw); max-height:80vh; overflow-y:auto; }
|
|
5559
|
+
.modal h3 { color:#e6edf3; margin-bottom:16px; font-size:14px; }
|
|
5560
|
+
.form-row { margin-bottom:12px; }
|
|
5561
|
+
.form-row label { display:block; font-size:12px; color:#8b949e; margin-bottom:4px; }
|
|
5562
|
+
.form-row input, .form-row textarea { width:100%; background:#0d1117; border:1px solid #30363d; border-radius:4px; color:#c9d1d9; font-family:monospace; font-size:13px; padding:6px 8px; }
|
|
5563
|
+
.form-row textarea { resize:vertical; min-height:60px; }
|
|
5564
|
+
.btn { padding:6px 14px; border-radius:4px; border:1px solid #30363d; cursor:pointer; font-family:monospace; font-size:13px; }
|
|
5565
|
+
.btn-primary { background:#1f6feb; color:#fff; border-color:#1f6feb; }
|
|
5566
|
+
.btn-danger { background:#da3633; color:#fff; border-color:#da3633; }
|
|
5567
|
+
.modal-footer { display:flex; gap:8px; justify-content:flex-end; margin-top:16px; }
|
|
5568
|
+
#upgrade-output { display:none; background:#0d1117; border:1px solid #30363d; border-radius:4px; padding:8px; font-family:monospace; font-size:12px; color:#c9d1d9; white-space:pre-wrap; max-height:200px; overflow-y:auto; margin-top:8px; }
|
|
5569
|
+
|
|
5570
|
+
/* \u2500\u2500 Mobile \u2500\u2500 */
|
|
5571
|
+
@media (max-width: 600px) {
|
|
5572
|
+
#hamburger { display: block; }
|
|
5573
|
+
#sidebar {
|
|
5574
|
+
position: fixed;
|
|
5575
|
+
top: 0; left: 0; bottom: 0;
|
|
5576
|
+
z-index: 50;
|
|
5577
|
+
transform: translateX(-100%);
|
|
5578
|
+
transition: transform .2s ease;
|
|
5579
|
+
}
|
|
5580
|
+
#sidebar.open { transform: translateX(0); }
|
|
5581
|
+
}
|
|
5297
5582
|
</style>
|
|
5298
5583
|
</head>
|
|
5299
5584
|
<body>
|
|
5300
|
-
<
|
|
5585
|
+
<div id="topbar">
|
|
5586
|
+
<button id="hamburger" aria-label="Toggle sidebar">\u2630</button>
|
|
5301
5587
|
<h1>\u26A1 ecode web admin</h1>
|
|
5302
5588
|
<span class="version">v${version2}</span>
|
|
5303
|
-
|
|
5589
|
+
<button id="config-btn" class="btn">\u914D\u7F6E</button>
|
|
5590
|
+
<button id="upgrade-btn" class="btn">\u5347\u7EA7</button>
|
|
5591
|
+
</div>
|
|
5304
5592
|
|
|
5305
|
-
<div
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
<div id="
|
|
5593
|
+
<div id="app">
|
|
5594
|
+
<!-- Left: session sidebar -->
|
|
5595
|
+
<div id="sidebar">
|
|
5596
|
+
<div id="sidebar-header">
|
|
5597
|
+
<button id="new-session-btn">\uFF0B \u65B0\u5EFA\u4F1A\u8BDD</button>
|
|
5598
|
+
</div>
|
|
5599
|
+
<div id="session-list">
|
|
5600
|
+
<div class="sidebar-empty">\u52A0\u8F7D\u4E2D\u2026</div>
|
|
5601
|
+
</div>
|
|
5309
5602
|
</div>
|
|
5310
5603
|
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
<div id="
|
|
5604
|
+
<!-- Right: chat area -->
|
|
5605
|
+
<div id="chat-area">
|
|
5606
|
+
<div id="chat-header">
|
|
5607
|
+
<span id="chat-title">\u9009\u62E9\u6216\u65B0\u5EFA\u4F1A\u8BDD</span>
|
|
5608
|
+
<div id="status-indicator">
|
|
5609
|
+
<div id="status-dot"></div>
|
|
5610
|
+
<span id="status-text">idle</span>
|
|
5611
|
+
</div>
|
|
5612
|
+
</div>
|
|
5613
|
+
<div id="messages">
|
|
5614
|
+
<div class="empty-chat">\u2190 \u4ECE\u5DE6\u4FA7\u9009\u62E9\u4F1A\u8BDD\uFF0C\u6216\u70B9\u51FB"\u65B0\u5EFA\u4F1A\u8BDD"\u5F00\u59CB</div>
|
|
5615
|
+
</div>
|
|
5616
|
+
<div id="ws-status" class="ws-status"></div>
|
|
5617
|
+
<div id="input-bar">
|
|
5618
|
+
<textarea id="msg-input" rows="1" placeholder="\u8F93\u5165\u6D88\u606F\uFF0C\u6309 Enter \u53D1\u9001\uFF08Shift+Enter \u6362\u884C\uFF09\u2026" disabled></textarea>
|
|
5619
|
+
<button id="send-btn" disabled>\u53D1\u9001</button>
|
|
5620
|
+
</div>
|
|
5314
5621
|
</div>
|
|
5622
|
+
</div>
|
|
5315
5623
|
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
<
|
|
5320
|
-
<div class="
|
|
5321
|
-
<div class="
|
|
5322
|
-
<div class="
|
|
5323
|
-
<div class="
|
|
5624
|
+
<!-- Config modal -->
|
|
5625
|
+
<div id="config-modal" class="modal-overlay">
|
|
5626
|
+
<div class="modal">
|
|
5627
|
+
<h3>\u2699 \u914D\u7F6E</h3>
|
|
5628
|
+
<div class="form-row"><label>Model</label><input id="cfg-model" name="model" type="text" /></div>
|
|
5629
|
+
<div class="form-row"><label>Base URL</label><input id="cfg-baseurl" name="baseUrl" type="text" /></div>
|
|
5630
|
+
<div class="form-row"><label>API Key</label><input id="cfg-apikey" name="apiKey" type="password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" /></div>
|
|
5631
|
+
<div class="form-row"><label>Log Dir</label><input id="cfg-logdir" name="logDir" type="text" /></div>
|
|
5632
|
+
<div class="form-row"><label>System Prompt</label><textarea id="cfg-systemprompt"></textarea></div>
|
|
5633
|
+
<div class="modal-footer">
|
|
5634
|
+
<button id="cancel-config" class="btn">\u53D6\u6D88</button>
|
|
5635
|
+
<button id="save-config" class="btn btn-primary">\u4FDD\u5B58</button>
|
|
5636
|
+
</div>
|
|
5637
|
+
</div>
|
|
5638
|
+
</div>
|
|
5639
|
+
|
|
5640
|
+
<!-- Upgrade modal -->
|
|
5641
|
+
<div id="upgrade-modal" class="modal-overlay">
|
|
5642
|
+
<div class="modal">
|
|
5643
|
+
<h3>\u2B06 \u5347\u7EA7 ecode</h3>
|
|
5644
|
+
<div id="upgrade-status">\u6B63\u5728\u68C0\u67E5\u7248\u672C\u2026</div>
|
|
5645
|
+
<div id="upgrade-output"></div>
|
|
5646
|
+
<div class="modal-footer">
|
|
5647
|
+
<button id="cancel-upgrade" class="btn">\u5173\u95ED</button>
|
|
5648
|
+
<button id="confirm-upgrade" class="btn btn-primary" disabled>\u5347\u7EA7</button>
|
|
5649
|
+
</div>
|
|
5650
|
+
</div>
|
|
5651
|
+
</div>
|
|
5652
|
+
|
|
5653
|
+
<!-- Bash approval modal -->
|
|
5654
|
+
<div id="approval-modal" role="dialog" aria-modal="true">
|
|
5655
|
+
<div id="approval-box">
|
|
5656
|
+
<div id="approval-title">\u26A0 \u8BF7\u6C42 Bash \u6267\u884C\u6743\u9650</div>
|
|
5657
|
+
<pre id="approval-prompt"></pre>
|
|
5658
|
+
<div id="approval-actions">
|
|
5659
|
+
<button id="deny-btn">\u62D2\u7EDD</button>
|
|
5660
|
+
<button id="approve-btn">\u5141\u8BB8</button>
|
|
5661
|
+
</div>
|
|
5324
5662
|
</div>
|
|
5325
5663
|
</div>
|
|
5326
5664
|
|
|
5327
5665
|
<script>
|
|
5328
|
-
|
|
5666
|
+
// \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5667
|
+
const state = {
|
|
5668
|
+
token: new URLSearchParams(location.search).get('token') || '',
|
|
5669
|
+
sessions: [],
|
|
5670
|
+
activeSessionId: null,
|
|
5671
|
+
messages: [],
|
|
5672
|
+
ws: null,
|
|
5673
|
+
pendingApproval: null,
|
|
5674
|
+
status: 'idle', // idle | thinking | tool_calling | awaiting_confirm
|
|
5675
|
+
wsRetries: 0,
|
|
5676
|
+
streamingMsgEl: null, // DOM element currently receiving delta tokens
|
|
5677
|
+
};
|
|
5329
5678
|
|
|
5330
|
-
|
|
5679
|
+
// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5680
|
+
function apiUrl(path) {
|
|
5331
5681
|
const sep = path.includes('?') ? '&' : '?';
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
}
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
// \u72B6\u6001\u5361
|
|
5342
|
-
apiFetch('/api/status').then(d => {
|
|
5343
|
-
const rows = [
|
|
5344
|
-
['\u72B6\u6001', '<span class="badge ok">\u8FD0\u884C\u4E2D</span>'],
|
|
5345
|
-
['\u7248\u672C', d.version ?? '-'],
|
|
5346
|
-
['\u6D3B\u8DC3\u4F1A\u8BDD', d.activeSessions ?? 0],
|
|
5347
|
-
];
|
|
5348
|
-
setHtml('status-content',
|
|
5349
|
-
rows.map(([l,v]) =>
|
|
5350
|
-
'<div class="stat"><span class="label">' + l + '</span><span class="value">' + v + '</span></div>'
|
|
5351
|
-
).join('')
|
|
5352
|
-
);
|
|
5353
|
-
}).catch(e => setHtml('status-content', '<span class="error">' + e.message + '</span>'));
|
|
5682
|
+
return path + sep + 'token=' + encodeURIComponent(state.token);
|
|
5683
|
+
}
|
|
5684
|
+
|
|
5685
|
+
async function apiFetch(path, opts = {}) {
|
|
5686
|
+
const url = apiUrl(path);
|
|
5687
|
+
const res = await fetch(url, opts);
|
|
5688
|
+
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
|
5689
|
+
return res.json();
|
|
5690
|
+
}
|
|
5354
5691
|
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5692
|
+
function wsUrl(sessionId) {
|
|
5693
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
5694
|
+
return proto + '//' + location.host + '/api/ws/sessions/' + sessionId + '?token=' + encodeURIComponent(state.token);
|
|
5695
|
+
}
|
|
5696
|
+
|
|
5697
|
+
// Basic markdown \u2192 HTML (no libs)
|
|
5698
|
+
function renderMarkdown(text) {
|
|
5699
|
+
// Escape HTML first
|
|
5700
|
+
let s = text
|
|
5701
|
+
.replace(/&/g, '&')
|
|
5702
|
+
.replace(/</g, '<')
|
|
5703
|
+
.replace(/>/g, '>');
|
|
5704
|
+
// Code blocks
|
|
5705
|
+
s = s.replace(/\`\`\`([\\s\\S]*?)\`\`\`/g, (_, code) =>
|
|
5706
|
+
'<pre><code>' + code.trim() + '</code></pre>');
|
|
5707
|
+
// Inline code
|
|
5708
|
+
s = s.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
|
|
5709
|
+
// Bold
|
|
5710
|
+
s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
5711
|
+
// Paragraph breaks
|
|
5712
|
+
s = s.replace(/\\n\\n+/g, '</p><p>');
|
|
5713
|
+
return '<p>' + s + '</p>';
|
|
5714
|
+
}
|
|
5715
|
+
|
|
5716
|
+
// \u2500\u2500 Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5717
|
+
function setStatus(status) {
|
|
5718
|
+
state.status = status;
|
|
5719
|
+
const dot = document.getElementById('status-dot');
|
|
5720
|
+
const txt = document.getElementById('status-text');
|
|
5721
|
+
dot.className = '';
|
|
5722
|
+
if (status === 'idle') {
|
|
5723
|
+
dot.style.background = '#3fb950';
|
|
5724
|
+
txt.textContent = 'idle';
|
|
5725
|
+
} else if (status === 'thinking') {
|
|
5726
|
+
dot.className = 'thinking';
|
|
5727
|
+
dot.style.background = '';
|
|
5728
|
+
txt.textContent = 'thinking\u2026';
|
|
5729
|
+
} else if (status === 'tool_calling') {
|
|
5730
|
+
dot.className = 'thinking';
|
|
5731
|
+
dot.style.background = '';
|
|
5732
|
+
txt.textContent = 'tool\u2026';
|
|
5733
|
+
} else if (status === 'awaiting_confirm') {
|
|
5734
|
+
dot.style.background = '#d29922';
|
|
5735
|
+
txt.textContent = 'awaiting approval';
|
|
5736
|
+
}
|
|
5737
|
+
const inputEl = document.getElementById('msg-input');
|
|
5738
|
+
const sendEl = document.getElementById('send-btn');
|
|
5739
|
+
const canType = status === 'idle' && state.activeSessionId;
|
|
5740
|
+
inputEl.disabled = !canType;
|
|
5741
|
+
sendEl.disabled = !canType;
|
|
5742
|
+
}
|
|
5743
|
+
|
|
5744
|
+
// \u2500\u2500 Sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5745
|
+
function renderSidebar() {
|
|
5746
|
+
const listEl = document.getElementById('session-list');
|
|
5747
|
+
if (!state.sessions.length) {
|
|
5748
|
+
listEl.innerHTML = '<div class="sidebar-empty">\u6682\u65E0\u4F1A\u8BDD</div>';
|
|
5360
5749
|
return;
|
|
5361
5750
|
}
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
'</div>'
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5751
|
+
listEl.innerHTML = state.sessions.map(s => {
|
|
5752
|
+
const active = s.id === state.activeSessionId ? ' active' : '';
|
|
5753
|
+
const title = (s.title || '(\u65E0\u6807\u9898)').replace(/</g, '<');
|
|
5754
|
+
const id = s.id.slice(0, 8);
|
|
5755
|
+
return '<div class="session-item' + active + '" data-id="' + s.id + '">' +
|
|
5756
|
+
'<div class="s-title">' + title + '</div>' +
|
|
5757
|
+
'<div class="s-id">' + id + '\u2026</div>' +
|
|
5758
|
+
'</div>';
|
|
5759
|
+
}).join('');
|
|
5760
|
+
listEl.querySelectorAll('.session-item').forEach(el => {
|
|
5761
|
+
el.addEventListener('click', () => selectSession(el.dataset.id));
|
|
5762
|
+
});
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
async function loadSessions() {
|
|
5766
|
+
try {
|
|
5767
|
+
const data = await apiFetch('/api/sessions');
|
|
5768
|
+
state.sessions = data.sessions || data.data || data || [];
|
|
5769
|
+
renderSidebar();
|
|
5770
|
+
} catch (e) {
|
|
5771
|
+
document.getElementById('session-list').innerHTML =
|
|
5772
|
+
'<div class="sidebar-empty" style="color:#f85149">\u52A0\u8F7D\u5931\u8D25</div>';
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
|
|
5776
|
+
// \u2500\u2500 Messages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5777
|
+
function clearMessages() {
|
|
5778
|
+
document.getElementById('messages').innerHTML =
|
|
5779
|
+
'<div class="empty-chat">\u2190 \u52A0\u8F7D\u4F1A\u8BDD\u4E2D\u2026</div>';
|
|
5780
|
+
state.messages = [];
|
|
5781
|
+
state.streamingMsgEl = null;
|
|
5782
|
+
}
|
|
5783
|
+
|
|
5784
|
+
function appendMessage(role, htmlContent, opts) {
|
|
5785
|
+
const msgsEl = document.getElementById('messages');
|
|
5786
|
+
// Remove empty-chat placeholder
|
|
5787
|
+
const placeholder = msgsEl.querySelector('.empty-chat');
|
|
5788
|
+
if (placeholder) placeholder.remove();
|
|
5789
|
+
|
|
5790
|
+
const msgEl = document.createElement('div');
|
|
5791
|
+
msgEl.className = 'msg';
|
|
5792
|
+
const prefix = role === 'user' ? '[user]' : role === 'tool' ? '[tool]' : '[assistant]';
|
|
5793
|
+
msgEl.innerHTML =
|
|
5794
|
+
'<div class="msg-header ' + role + '">' + prefix + '</div>' +
|
|
5795
|
+
'<div class="msg-body">' + htmlContent + '</div>';
|
|
5796
|
+
if (opts && opts.streaming) {
|
|
5797
|
+
msgEl.querySelector('.msg-body').classList.add('cursor-blink');
|
|
5798
|
+
}
|
|
5799
|
+
msgsEl.appendChild(msgEl);
|
|
5800
|
+
msgsEl.scrollTop = msgsEl.scrollHeight;
|
|
5801
|
+
return msgEl;
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5804
|
+
function finalizeStreamingMsg() {
|
|
5805
|
+
if (state.streamingMsgEl) {
|
|
5806
|
+
const body = state.streamingMsgEl.querySelector('.msg-body');
|
|
5807
|
+
if (body) body.classList.remove('cursor-blink');
|
|
5808
|
+
state.streamingMsgEl = null;
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
|
|
5812
|
+
// \u2500\u2500 WebSocket \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5813
|
+
function connectWs(sessionId) {
|
|
5814
|
+
if (state.ws) {
|
|
5815
|
+
state.ws.onclose = null;
|
|
5816
|
+
state.ws.close();
|
|
5817
|
+
state.ws = null;
|
|
5818
|
+
}
|
|
5819
|
+
state.wsRetries = 0;
|
|
5820
|
+
openWs(sessionId);
|
|
5821
|
+
}
|
|
5822
|
+
|
|
5823
|
+
function openWs(sessionId) {
|
|
5824
|
+
const url = wsUrl(sessionId);
|
|
5825
|
+
const ws = new WebSocket(url);
|
|
5826
|
+
state.ws = ws;
|
|
5827
|
+
|
|
5828
|
+
document.getElementById('ws-status').textContent = '\u8FDE\u63A5\u4E2D\u2026';
|
|
5829
|
+
|
|
5830
|
+
ws.onopen = () => {
|
|
5831
|
+
document.getElementById('ws-status').textContent = '';
|
|
5832
|
+
state.wsRetries = 0;
|
|
5833
|
+
setStatus('idle');
|
|
5834
|
+
};
|
|
5835
|
+
|
|
5836
|
+
ws.onmessage = (evt) => {
|
|
5837
|
+
let msg;
|
|
5838
|
+
try { msg = JSON.parse(evt.data); } catch { return; }
|
|
5839
|
+
handleWsEvent(msg);
|
|
5840
|
+
};
|
|
5841
|
+
|
|
5842
|
+
ws.onerror = () => {
|
|
5843
|
+
document.getElementById('ws-status').textContent = 'WebSocket \u9519\u8BEF';
|
|
5844
|
+
};
|
|
5845
|
+
|
|
5846
|
+
ws.onclose = () => {
|
|
5847
|
+
document.getElementById('ws-status').textContent = 'WebSocket \u5DF2\u65AD\u5F00';
|
|
5848
|
+
finalizeStreamingMsg();
|
|
5849
|
+
if (state.activeSessionId === sessionId && state.wsRetries < 5) {
|
|
5850
|
+
state.wsRetries++;
|
|
5851
|
+
setTimeout(() => openWs(sessionId), 3000);
|
|
5852
|
+
}
|
|
5853
|
+
};
|
|
5854
|
+
}
|
|
5855
|
+
|
|
5856
|
+
function handleWsEvent(msg) {
|
|
5857
|
+
const type = msg.type || msg.event;
|
|
5858
|
+
if (type === 'message.delta') {
|
|
5859
|
+
const token = msg.delta || msg.content || '';
|
|
5860
|
+
if (!state.streamingMsgEl) {
|
|
5861
|
+
state.streamingMsgEl = appendMessage('assistant', escHtml(token), { streaming: true });
|
|
5862
|
+
} else {
|
|
5863
|
+
const body = state.streamingMsgEl.querySelector('.msg-body');
|
|
5864
|
+
body.textContent += token;
|
|
5865
|
+
document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
|
|
5866
|
+
}
|
|
5867
|
+
setStatus('thinking');
|
|
5868
|
+
} else if (type === 'message.completed') {
|
|
5869
|
+
finalizeStreamingMsg();
|
|
5870
|
+
setStatus('idle');
|
|
5871
|
+
} else if (type === 'tool.started') {
|
|
5872
|
+
const name = msg.tool || msg.name || 'tool';
|
|
5873
|
+
const msgsEl = document.getElementById('messages');
|
|
5874
|
+
const placeholder = msgsEl.querySelector('.empty-chat');
|
|
5875
|
+
if (placeholder) placeholder.remove();
|
|
5876
|
+
const boxEl = document.createElement('div');
|
|
5877
|
+
boxEl.className = 'tool-box';
|
|
5878
|
+
boxEl.setAttribute('data-tool-id', msg.id || name);
|
|
5879
|
+
boxEl.innerHTML =
|
|
5880
|
+
'<div class="tool-box-header">\u250C\u2500 ' + escHtml(name) + ' \u2500</div>' +
|
|
5881
|
+
'<div class="tool-box-body">\u2026</div>';
|
|
5882
|
+
msgsEl.appendChild(boxEl);
|
|
5883
|
+
msgsEl.scrollTop = msgsEl.scrollHeight;
|
|
5884
|
+
setStatus('tool_calling');
|
|
5885
|
+
} else if (type === 'tool.completed') {
|
|
5886
|
+
const toolId = msg.id || msg.tool || msg.name || '';
|
|
5887
|
+
const boxEl = document.querySelector('[data-tool-id="' + toolId + '"]');
|
|
5888
|
+
if (boxEl) {
|
|
5889
|
+
const body = boxEl.querySelector('.tool-box-body');
|
|
5890
|
+
body.textContent = msg.output || msg.result || '(\uC644\uB8CC)';
|
|
5891
|
+
boxEl.querySelector('.tool-box-header').textContent =
|
|
5892
|
+
'\u2514\u2500 ' + (msg.tool || msg.name || toolId) + ' \u2500';
|
|
5893
|
+
}
|
|
5894
|
+
setStatus('idle');
|
|
5895
|
+
} else if (type === 'approval.requested') {
|
|
5896
|
+
state.pendingApproval = msg;
|
|
5897
|
+
showApprovalModal(msg);
|
|
5898
|
+
setStatus('awaiting_confirm');
|
|
5899
|
+
} else if (type === 'approval.resolved') {
|
|
5900
|
+
hideApprovalModal();
|
|
5901
|
+
state.pendingApproval = null;
|
|
5902
|
+
setStatus('thinking');
|
|
5903
|
+
} else if (type === 'session.idle') {
|
|
5904
|
+
finalizeStreamingMsg();
|
|
5905
|
+
setStatus('idle');
|
|
5906
|
+
} else if (type === 'session.error') {
|
|
5907
|
+
finalizeStreamingMsg();
|
|
5908
|
+
setStatus('idle');
|
|
5909
|
+
document.getElementById('ws-status').textContent =
|
|
5910
|
+
'\u9519\u8BEF: ' + escHtml(msg.error || 'unknown error');
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
|
|
5914
|
+
function escHtml(s) {
|
|
5915
|
+
return String(s)
|
|
5916
|
+
.replace(/&/g, '&')
|
|
5917
|
+
.replace(/</g, '<')
|
|
5918
|
+
.replace(/>/g, '>');
|
|
5919
|
+
}
|
|
5920
|
+
|
|
5921
|
+
// \u2500\u2500 Approval modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5922
|
+
function showApprovalModal(msg) {
|
|
5923
|
+
const box = document.getElementById('approval-box');
|
|
5924
|
+
box.className = msg.kind === 'danger' ? 'danger' : '';
|
|
5925
|
+
document.getElementById('approval-prompt').textContent =
|
|
5926
|
+
msg.prompt || msg.command || msg.text || JSON.stringify(msg);
|
|
5927
|
+
document.getElementById('approval-modal').classList.add('visible');
|
|
5928
|
+
}
|
|
5929
|
+
|
|
5930
|
+
function hideApprovalModal() {
|
|
5931
|
+
document.getElementById('approval-modal').classList.remove('visible');
|
|
5932
|
+
}
|
|
5933
|
+
|
|
5934
|
+
async function sendApproval(approved) {
|
|
5935
|
+
const pending = state.pendingApproval;
|
|
5936
|
+
if (!pending || !state.activeSessionId) return;
|
|
5937
|
+
hideApprovalModal();
|
|
5938
|
+
state.pendingApproval = null;
|
|
5939
|
+
try {
|
|
5940
|
+
await apiFetch('/api/chat/sessions/' + state.activeSessionId + '/approve', {
|
|
5941
|
+
method: 'POST',
|
|
5942
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5943
|
+
body: JSON.stringify({ requestId: pending.requestId || pending.id, approved }),
|
|
5944
|
+
});
|
|
5945
|
+
} catch (e) {
|
|
5946
|
+
document.getElementById('ws-status').textContent = '\u5BA1\u6279\u8BF7\u6C42\u5931\u8D25: ' + e.message;
|
|
5947
|
+
}
|
|
5948
|
+
}
|
|
5949
|
+
|
|
5950
|
+
document.getElementById('approve-btn').addEventListener('click', () => sendApproval(true));
|
|
5951
|
+
document.getElementById('deny-btn').addEventListener('click', () => sendApproval(false));
|
|
5952
|
+
|
|
5953
|
+
// \u2500\u2500 Session selection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5954
|
+
function selectSession(id) {
|
|
5955
|
+
state.activeSessionId = id;
|
|
5956
|
+
const session = state.sessions.find(s => s.id === id);
|
|
5957
|
+
document.getElementById('chat-title').textContent =
|
|
5958
|
+
session ? (session.title || id.slice(0, 12) + '\u2026') : id;
|
|
5959
|
+
clearMessages();
|
|
5960
|
+
renderSidebar();
|
|
5961
|
+
setStatus('idle');
|
|
5962
|
+
connectWs(id);
|
|
5963
|
+
// Close sidebar on mobile
|
|
5964
|
+
document.getElementById('sidebar').classList.remove('open');
|
|
5965
|
+
}
|
|
5966
|
+
|
|
5967
|
+
// \u2500\u2500 New session \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5968
|
+
document.getElementById('new-session-btn').addEventListener('click', async () => {
|
|
5969
|
+
try {
|
|
5970
|
+
const data = await apiFetch('/api/chat/sessions', {
|
|
5971
|
+
method: 'POST',
|
|
5972
|
+
headers: { 'Content-Type': 'application/json' },
|
|
5973
|
+
body: JSON.stringify({}),
|
|
5974
|
+
});
|
|
5975
|
+
const session = data.session || data;
|
|
5976
|
+
if (session && session.id) {
|
|
5977
|
+
state.sessions.unshift(session);
|
|
5978
|
+
renderSidebar();
|
|
5979
|
+
selectSession(session.id);
|
|
5980
|
+
}
|
|
5981
|
+
} catch (e) {
|
|
5982
|
+
document.getElementById('ws-status').textContent = '\u521B\u5EFA\u4F1A\u8BDD\u5931\u8D25: ' + e.message;
|
|
5983
|
+
}
|
|
5984
|
+
});
|
|
5985
|
+
|
|
5986
|
+
// \u2500\u2500 Send message \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
5987
|
+
async function sendMessage() {
|
|
5988
|
+
if (state.status !== 'idle' || !state.activeSessionId) return;
|
|
5989
|
+
const inputEl = document.getElementById('msg-input');
|
|
5990
|
+
const text = inputEl.value.trim();
|
|
5991
|
+
if (!text) return;
|
|
5992
|
+
inputEl.value = '';
|
|
5993
|
+
inputEl.style.height = 'auto';
|
|
5994
|
+
appendMessage('user', escHtml(text));
|
|
5995
|
+
setStatus('thinking');
|
|
5996
|
+
try {
|
|
5997
|
+
await apiFetch('/api/chat/sessions/' + state.activeSessionId + '/submit', {
|
|
5998
|
+
method: 'POST',
|
|
5999
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6000
|
+
body: JSON.stringify({ message: text }),
|
|
6001
|
+
});
|
|
6002
|
+
} catch (e) {
|
|
6003
|
+
document.getElementById('ws-status').textContent = '\u53D1\u9001\u5931\u8D25: ' + e.message;
|
|
6004
|
+
setStatus('idle');
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
|
|
6008
|
+
document.getElementById('send-btn').addEventListener('click', sendMessage);
|
|
6009
|
+
|
|
6010
|
+
document.getElementById('msg-input').addEventListener('keydown', (e) => {
|
|
6011
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
6012
|
+
e.preventDefault();
|
|
6013
|
+
sendMessage();
|
|
6014
|
+
}
|
|
6015
|
+
});
|
|
6016
|
+
|
|
6017
|
+
// Auto-resize textarea
|
|
6018
|
+
document.getElementById('msg-input').addEventListener('input', function() {
|
|
6019
|
+
this.style.height = 'auto';
|
|
6020
|
+
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
|
6021
|
+
});
|
|
6022
|
+
|
|
6023
|
+
// \u2500\u2500 Hamburger (mobile) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6024
|
+
document.getElementById('hamburger').addEventListener('click', () => {
|
|
6025
|
+
document.getElementById('sidebar').classList.toggle('open');
|
|
6026
|
+
});
|
|
6027
|
+
|
|
6028
|
+
// \u2500\u2500 \u914D\u7F6E\u6A21\u6001\u6846 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6029
|
+
document.getElementById('config-btn').addEventListener('click', async () => {
|
|
6030
|
+
try {
|
|
6031
|
+
const data = await apiFetch('/api/config');
|
|
6032
|
+
document.getElementById('cfg-model').value = data.model || '';
|
|
6033
|
+
document.getElementById('cfg-baseurl').value = data.baseUrl || '';
|
|
6034
|
+
document.getElementById('cfg-logdir').value = data.logDir || '';
|
|
6035
|
+
document.getElementById('cfg-systemprompt').value = data.systemPrompt || '';
|
|
6036
|
+
} catch (e) {
|
|
6037
|
+
// open modal even if fetch fails; fields will be empty
|
|
6038
|
+
}
|
|
6039
|
+
document.getElementById('config-modal').classList.add('open');
|
|
6040
|
+
});
|
|
6041
|
+
|
|
6042
|
+
document.getElementById('cancel-config').addEventListener('click', () => {
|
|
6043
|
+
document.getElementById('config-modal').classList.remove('open');
|
|
6044
|
+
});
|
|
6045
|
+
|
|
6046
|
+
document.getElementById('save-config').addEventListener('click', async () => {
|
|
6047
|
+
const body = {
|
|
6048
|
+
model: document.getElementById('cfg-model').value,
|
|
6049
|
+
baseUrl: document.getElementById('cfg-baseurl').value,
|
|
6050
|
+
logDir: document.getElementById('cfg-logdir').value,
|
|
6051
|
+
systemPrompt: document.getElementById('cfg-systemprompt').value,
|
|
6052
|
+
};
|
|
6053
|
+
const apiKeyVal = document.getElementById('cfg-apikey').value;
|
|
6054
|
+
if (apiKeyVal) body.apiKey = apiKeyVal;
|
|
6055
|
+
try {
|
|
6056
|
+
await apiFetch('/api/config', { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
|
|
6057
|
+
} catch (e) {
|
|
6058
|
+
// ignore errors silently for now
|
|
6059
|
+
}
|
|
6060
|
+
document.getElementById('config-modal').classList.remove('open');
|
|
6061
|
+
});
|
|
6062
|
+
|
|
6063
|
+
// \u2500\u2500 \u5347\u7EA7 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6064
|
+
document.getElementById('upgrade-btn').addEventListener('click', async () => {
|
|
6065
|
+
const modal = document.getElementById('upgrade-modal');
|
|
6066
|
+
const statusEl = document.getElementById('upgrade-status');
|
|
6067
|
+
const outputEl = document.getElementById('upgrade-output');
|
|
6068
|
+
const confirmBtn = document.getElementById('confirm-upgrade');
|
|
6069
|
+
outputEl.style.display = 'none';
|
|
6070
|
+
outputEl.textContent = '';
|
|
6071
|
+
confirmBtn.disabled = true;
|
|
6072
|
+
statusEl.textContent = '\u6B63\u5728\u68C0\u67E5\u7248\u672C\u2026';
|
|
6073
|
+
modal.classList.add('open');
|
|
6074
|
+
try {
|
|
6075
|
+
const r = await fetch(apiUrl('/api/version/check'));
|
|
6076
|
+
const v = await r.json();
|
|
6077
|
+
if (v.needsUpdate) {
|
|
6078
|
+
statusEl.textContent = '\u5F53\u524D v' + v.current + '\uFF0C\u6700\u65B0 v' + v.latest + '\uFF0C\u786E\u8BA4\u5347\u7EA7\uFF1F';
|
|
6079
|
+
confirmBtn.disabled = false;
|
|
6080
|
+
} else {
|
|
6081
|
+
statusEl.textContent = '\u5DF2\u662F\u6700\u65B0\u7248\u672C v' + v.current;
|
|
6082
|
+
}
|
|
6083
|
+
} catch(e) {
|
|
6084
|
+
statusEl.textContent = '\u7248\u672C\u68C0\u67E5\u5931\u8D25\uFF1A' + e.message;
|
|
6085
|
+
}
|
|
6086
|
+
});
|
|
6087
|
+
|
|
6088
|
+
document.getElementById('cancel-upgrade').addEventListener('click', () => {
|
|
6089
|
+
document.getElementById('upgrade-modal').classList.remove('open');
|
|
6090
|
+
});
|
|
6091
|
+
|
|
6092
|
+
document.getElementById('confirm-upgrade').addEventListener('click', async () => {
|
|
6093
|
+
const outputEl = document.getElementById('upgrade-output');
|
|
6094
|
+
const confirmBtn = document.getElementById('confirm-upgrade');
|
|
6095
|
+
const statusEl = document.getElementById('upgrade-status');
|
|
6096
|
+
confirmBtn.disabled = true;
|
|
6097
|
+
outputEl.style.display = 'block';
|
|
6098
|
+
outputEl.textContent = '';
|
|
6099
|
+
statusEl.textContent = '\u5347\u7EA7\u4E2D\u2026';
|
|
6100
|
+
try {
|
|
6101
|
+
const r = await fetch(apiUrl('/api/system/upgrade'), { method: 'POST' });
|
|
6102
|
+
const reader = r.body.getReader();
|
|
6103
|
+
const decoder = new TextDecoder();
|
|
6104
|
+
while (true) {
|
|
6105
|
+
const { done, value } = await reader.read();
|
|
6106
|
+
if (done) break;
|
|
6107
|
+
outputEl.textContent += decoder.decode(value);
|
|
6108
|
+
outputEl.scrollTop = outputEl.scrollHeight;
|
|
6109
|
+
}
|
|
6110
|
+
statusEl.textContent = '\u5347\u7EA7\u5B8C\u6210\uFF0C\u8BF7\u91CD\u542F ecode web';
|
|
6111
|
+
} catch(e) {
|
|
6112
|
+
statusEl.textContent = '\u5347\u7EA7\u5931\u8D25\uFF1A' + e.message;
|
|
6113
|
+
}
|
|
6114
|
+
});
|
|
6115
|
+
|
|
6116
|
+
// \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
6117
|
+
loadSessions();
|
|
5371
6118
|
</script>
|
|
5372
6119
|
</body>
|
|
5373
6120
|
</html>`;
|
|
@@ -5666,6 +6413,42 @@ async function sessionHubRoutes(app, opts) {
|
|
|
5666
6413
|
);
|
|
5667
6414
|
}
|
|
5668
6415
|
|
|
6416
|
+
// src/web/routes/system.ts
|
|
6417
|
+
import { execSync } from "child_process";
|
|
6418
|
+
import { spawn } from "child_process";
|
|
6419
|
+
async function systemRoutes(app, opts) {
|
|
6420
|
+
app.get("/api/version/check", async (_request, _reply) => {
|
|
6421
|
+
const current = opts.version;
|
|
6422
|
+
let latest;
|
|
6423
|
+
try {
|
|
6424
|
+
latest = execSync("npm view @zhongqian97-code/ecode version", {
|
|
6425
|
+
encoding: "utf-8",
|
|
6426
|
+
timeout: 5e3
|
|
6427
|
+
}).trim();
|
|
6428
|
+
} catch {
|
|
6429
|
+
latest = "unknown";
|
|
6430
|
+
}
|
|
6431
|
+
const needsUpdate = latest !== "unknown" && latest !== current;
|
|
6432
|
+
return { current, latest, needsUpdate };
|
|
6433
|
+
});
|
|
6434
|
+
app.post("/api/system/upgrade", (_request, reply) => {
|
|
6435
|
+
reply.raw.setHeader("Content-Type", "text/plain");
|
|
6436
|
+
reply.raw.write("Upgrading @zhongqian97-code/ecode...\n");
|
|
6437
|
+
const child = spawn("npm", ["update", "-g", "@zhongqian97-code/ecode"]);
|
|
6438
|
+
child.stdout.on("data", (chunk) => {
|
|
6439
|
+
reply.raw.write(chunk);
|
|
6440
|
+
});
|
|
6441
|
+
child.stderr.on("data", (chunk) => {
|
|
6442
|
+
reply.raw.write(chunk);
|
|
6443
|
+
});
|
|
6444
|
+
child.on("close", () => {
|
|
6445
|
+
reply.raw.write("Upgrade complete. Please restart ecode web.\n");
|
|
6446
|
+
reply.raw.end();
|
|
6447
|
+
});
|
|
6448
|
+
return reply;
|
|
6449
|
+
});
|
|
6450
|
+
}
|
|
6451
|
+
|
|
5669
6452
|
// src/web/server.ts
|
|
5670
6453
|
async function buildServer(opts) {
|
|
5671
6454
|
const app = Fastify({ logger: false });
|
|
@@ -5693,6 +6476,7 @@ async function buildServer(opts) {
|
|
|
5693
6476
|
await app.register(configRoutes, { config: opts.config });
|
|
5694
6477
|
await app.register(automationRoutes, { config: opts.config });
|
|
5695
6478
|
await app.register(chatRoutes, { config: opts.config, manager: opts.manager });
|
|
6479
|
+
await app.register(systemRoutes, { version: opts.version });
|
|
5696
6480
|
await app.register(sessionHubRoutes, { manager: opts.manager });
|
|
5697
6481
|
return app;
|
|
5698
6482
|
}
|