@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.
Files changed (2) hide show
  1. package/dist/index.js +877 -93
  2. 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: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
5259
- background: #0d1117; color: #c9d1d9; min-height: 100vh; padding: 24px;
5260
- }
5261
- header {
5262
- display: flex; align-items: center; gap: 12px;
5263
- padding-bottom: 16px; border-bottom: 1px solid #21262d; margin-bottom: 24px;
5264
- }
5265
- header h1 { font-size: 18px; font-weight: 600; color: #e6edf3; }
5266
- header .version { font-size: 12px; color: #8b949e; background: #21262d;
5267
- padding: 2px 8px; border-radius: 20px; }
5268
- .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; }
5269
- .card {
5270
- background: #161b22; border: 1px solid #21262d; border-radius: 8px; padding: 16px;
5271
- }
5272
- .card h2 { font-size: 13px; font-weight: 600; color: #8b949e;
5273
- text-transform: uppercase; letter-spacing: .05em; margin-bottom: 12px; }
5274
- .stat { display: flex; justify-content: space-between; align-items: center;
5275
- padding: 6px 0; border-bottom: 1px solid #21262d; font-size: 13px; }
5276
- .stat:last-child { border-bottom: none; }
5277
- .stat .label { color: #8b949e; }
5278
- .stat .value { color: #e6edf3; font-weight: 500; }
5279
- .badge { display: inline-block; padding: 2px 8px; border-radius: 20px;
5280
- font-size: 11px; font-weight: 600; }
5281
- .badge.ok { background: #1a4731; color: #3fb950; }
5282
- .badge.err { background: #4d1f24; color: #f85149; }
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 0; border-bottom: 1px solid #21262d; font-size: 13px;
5285
- }
5286
- .session-item:last-child { border-bottom: none; }
5287
- .session-id { color: #79c0ff; font-family: monospace; font-size: 12px; }
5288
- .session-title { color: #e6edf3; margin-top: 2px; }
5289
- .empty { color: #8b949e; font-size: 13px; font-style: italic; }
5290
- .error { color: #f85149; font-size: 13px; }
5291
- .loading { color: #8b949e; font-size: 13px; font-style: italic; }
5292
- .api-row { font-size: 12px; font-family: monospace; padding: 4px 0;
5293
- border-bottom: 1px solid #21262d; display: flex; gap: 8px; }
5294
- .api-row:last-child { border-bottom: none; }
5295
- .method { color: #79c0ff; min-width: 50px; }
5296
- .path { color: #e6edf3; }
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
- <header>
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
- </header>
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 class="grid">
5306
- <div class="card">
5307
- <h2>\u670D\u52A1\u72B6\u6001</h2>
5308
- <div id="status-content"><span class="loading">\u52A0\u8F7D\u4E2D\u2026</span></div>
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
- <div class="card">
5312
- <h2>\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD</h2>
5313
- <div id="sessions-content"><span class="loading">\u52A0\u8F7D\u4E2D\u2026</span></div>
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
- <div class="card">
5317
- <h2>API \u7AEF\u70B9</h2>
5318
- <div class="api-row"><span class="method">GET</span><span class="path">/api/status</span></div>
5319
- <div class="api-row"><span class="method">GET</span><span class="path">/api/sessions</span></div>
5320
- <div class="api-row"><span class="method">GET</span><span class="path">/api/config</span></div>
5321
- <div class="api-row"><span class="method">GET</span><span class="path">/api/automation/jobs</span></div>
5322
- <div class="api-row"><span class="method">POST</span><span class="path">/api/chat/sessions</span></div>
5323
- <div class="api-row"><span class="method">WS</span><span class="path">/api/ws/sessions/:id</span></div>
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
- const token = new URLSearchParams(location.search).get('token') || '';
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
- async function apiFetch(path) {
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
- const r = await fetch(path + sep + 'token=' + encodeURIComponent(token));
5333
- if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
5334
- return r.json();
5335
- }
5336
-
5337
- function setHtml(id, html) {
5338
- document.getElementById(id).innerHTML = html;
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
- // \u4F1A\u8BDD\u5217\u8868
5356
- apiFetch('/api/sessions').then(d => {
5357
- const sessions = d.sessions ?? d.data ?? d ?? [];
5358
- if (!sessions.length) {
5359
- setHtml('sessions-content', '<span class="empty">\u6682\u65E0\u4F1A\u8BDD</span>');
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, '&amp;')
5702
+ .replace(/</g, '&lt;')
5703
+ .replace(/>/g, '&gt;');
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
- setHtml('sessions-content',
5363
- sessions.slice(0, 10).map(s =>
5364
- '<div class="session-item">' +
5365
- '<div class="session-id">' + s.id + '</div>' +
5366
- '<div class="session-title">' + (s.title || '(\u65E0\u6807\u9898)') + '</div>' +
5367
- '</div>'
5368
- ).join('')
5369
- );
5370
- }).catch(e => setHtml('sessions-content', '<span class="error">' + e.message + '</span>'));
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, '&lt;');
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, '&amp;')
5917
+ .replace(/</g, '&lt;')
5918
+ .replace(/>/g, '&gt;');
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhongqian97-code/ecode",
3
- "version": "0.5.19",
3
+ "version": "0.5.21",
4
4
  "description": "A minimal Claude Code clone with REPL interface and bash tool calling",
5
5
  "type": "module",
6
6
  "author": "zhongqian97-code",