@zhongqian97-code/ecode 0.5.20 → 0.5.22

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 +243 -5
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5250,7 +5250,7 @@ function generateAdminHtml(version2) {
5250
5250
  <html lang="zh">
5251
5251
  <head>
5252
5252
  <meta charset="UTF-8">
5253
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
5253
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content">
5254
5254
  <title>ecode web admin</title>
5255
5255
  <style>
5256
5256
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -5552,6 +5552,28 @@ function generateAdminHtml(version2) {
5552
5552
  }
5553
5553
  #approve-btn:hover { background: #2ea043; color: #fff; }
5554
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 Syntax highlight \u2500\u2500 */
5571
+ .kw { color: #ff7b72; }
5572
+ .str { color: #a5d6ff; }
5573
+ .cmt { color: #8b949e; font-style: italic; }
5574
+ .num { color: #79c0ff; }
5575
+ .fn { color: #d2a8ff; }
5576
+
5555
5577
  /* \u2500\u2500 Mobile \u2500\u2500 */
5556
5578
  @media (max-width: 600px) {
5557
5579
  #hamburger { display: block; }
@@ -5571,6 +5593,8 @@ function generateAdminHtml(version2) {
5571
5593
  <button id="hamburger" aria-label="Toggle sidebar">\u2630</button>
5572
5594
  <h1>\u26A1 ecode web admin</h1>
5573
5595
  <span class="version">v${version2}</span>
5596
+ <button id="config-btn" class="btn">\u914D\u7F6E</button>
5597
+ <button id="upgrade-btn" class="btn">\u5347\u7EA7</button>
5574
5598
  </div>
5575
5599
 
5576
5600
  <div id="app">
@@ -5604,6 +5628,35 @@ function generateAdminHtml(version2) {
5604
5628
  </div>
5605
5629
  </div>
5606
5630
 
5631
+ <!-- Config modal -->
5632
+ <div id="config-modal" class="modal-overlay">
5633
+ <div class="modal">
5634
+ <h3>\u2699 \u914D\u7F6E</h3>
5635
+ <div class="form-row"><label>Model</label><input id="cfg-model" name="model" type="text" /></div>
5636
+ <div class="form-row"><label>Base URL</label><input id="cfg-baseurl" name="baseUrl" type="text" /></div>
5637
+ <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>
5638
+ <div class="form-row"><label>Log Dir</label><input id="cfg-logdir" name="logDir" type="text" /></div>
5639
+ <div class="form-row"><label>System Prompt</label><textarea id="cfg-systemprompt"></textarea></div>
5640
+ <div class="modal-footer">
5641
+ <button id="cancel-config" class="btn">\u53D6\u6D88</button>
5642
+ <button id="save-config" class="btn btn-primary">\u4FDD\u5B58</button>
5643
+ </div>
5644
+ </div>
5645
+ </div>
5646
+
5647
+ <!-- Upgrade modal -->
5648
+ <div id="upgrade-modal" class="modal-overlay">
5649
+ <div class="modal">
5650
+ <h3>\u2B06 \u5347\u7EA7 ecode</h3>
5651
+ <div id="upgrade-status">\u6B63\u5728\u68C0\u67E5\u7248\u672C\u2026</div>
5652
+ <div id="upgrade-output"></div>
5653
+ <div class="modal-footer">
5654
+ <button id="cancel-upgrade" class="btn">\u5173\u95ED</button>
5655
+ <button id="confirm-upgrade" class="btn btn-primary" disabled>\u5347\u7EA7</button>
5656
+ </div>
5657
+ </div>
5658
+ </div>
5659
+
5607
5660
  <!-- Bash approval modal -->
5608
5661
  <div id="approval-modal" role="dialog" aria-modal="true">
5609
5662
  <div id="approval-box">
@@ -5636,7 +5689,7 @@ function generateAdminHtml(version2) {
5636
5689
  return path + sep + 'token=' + encodeURIComponent(state.token);
5637
5690
  }
5638
5691
 
5639
- async function apiFetch(path, opts) {
5692
+ async function apiFetch(path, opts = {}) {
5640
5693
  const url = apiUrl(path);
5641
5694
  const res = await fetch(url, opts);
5642
5695
  if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
@@ -5648,6 +5701,21 @@ function generateAdminHtml(version2) {
5648
5701
  return proto + '//' + location.host + '/api/ws/sessions/' + sessionId + '?token=' + encodeURIComponent(state.token);
5649
5702
  }
5650
5703
 
5704
+ // \u2500\u2500 Code highlighting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5705
+ function highlightCode(code) {
5706
+ let s = escHtml(code);
5707
+ // keywords
5708
+ s = s.replace(/\b(function|const|let|var|return|if|else|for|while|class|import|export|from|async|await|new|this|typeof|instanceof|null|undefined|true|false)\b/g, '<span class="kw">$1</span>');
5709
+ // strings (single, double \u2014 after HTML escaping, quotes appear as &#39; or &quot;)
5710
+ s = s.replace(/(&#39;[^&#]*&#39;|&quot;[^&]*&quot;)/g, '<span class="str">$1</span>');
5711
+ // comments
5712
+ s = s.replace(/(//[^
5713
+ ]*)/g, '<span class="cmt">$1</span>');
5714
+ // numbers
5715
+ s = s.replace(/\b(d+.?d*)\b/g, '<span class="num">$1</span>');
5716
+ return s;
5717
+ }
5718
+
5651
5719
  // Basic markdown \u2192 HTML (no libs)
5652
5720
  function renderMarkdown(text) {
5653
5721
  // Escape HTML first
@@ -5655,9 +5723,9 @@ function generateAdminHtml(version2) {
5655
5723
  .replace(/&/g, '&amp;')
5656
5724
  .replace(/</g, '&lt;')
5657
5725
  .replace(/>/g, '&gt;');
5658
- // Code blocks
5726
+ // Code blocks (use highlightCode for syntax coloring)
5659
5727
  s = s.replace(/\`\`\`([\\s\\S]*?)\`\`\`/g, (_, code) =>
5660
- '<pre><code>' + code.trim() + '</code></pre>');
5728
+ '<pre><code>' + highlightCode(code.trim()) + '</code></pre>');
5661
5729
  // Inline code
5662
5730
  s = s.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
5663
5731
  // Bold
@@ -5735,6 +5803,31 @@ function generateAdminHtml(version2) {
5735
5803
  state.streamingMsgEl = null;
5736
5804
  }
5737
5805
 
5806
+ async function loadMessages(sessionId) {
5807
+ try {
5808
+ const sep = '/api/sessions/' + sessionId + '/messages?token=' + encodeURIComponent(state.token);
5809
+ const res = await fetch(sep);
5810
+ if (!res.ok) return;
5811
+ const text = await res.text();
5812
+ const lines = text.split('\\n').filter(l => l.trim());
5813
+ const msgsEl = document.getElementById('messages');
5814
+ msgsEl.innerHTML = '';
5815
+ for (const line of lines) {
5816
+ try {
5817
+ const m = JSON.parse(line);
5818
+ if (m.content) {
5819
+ appendMessage(m.role === 'user' ? 'user' : 'assistant', renderMarkdown(m.content));
5820
+ }
5821
+ } catch { /* skip malformed lines */ }
5822
+ }
5823
+ if (!lines.length) {
5824
+ msgsEl.innerHTML = '<div class="empty-chat">\u6682\u65E0\u5386\u53F2\u6D88\u606F\uFF0C\u5F00\u59CB\u5BF9\u8BDD\u5427</div>';
5825
+ }
5826
+ } catch (e) {
5827
+ // silently ignore \u2014 session might have no log file yet
5828
+ }
5829
+ }
5830
+
5738
5831
  function appendMessage(role, htmlContent, opts) {
5739
5832
  const msgsEl = document.getElementById('messages');
5740
5833
  // Remove empty-chat placeholder
@@ -5802,7 +5895,12 @@ function generateAdminHtml(version2) {
5802
5895
  finalizeStreamingMsg();
5803
5896
  if (state.activeSessionId === sessionId && state.wsRetries < 5) {
5804
5897
  state.wsRetries++;
5805
- setTimeout(() => openWs(sessionId), 3000);
5898
+ const wsRetryDelay = Math.min(1000 * Math.pow(2, state.wsRetries), 30000);
5899
+ document.getElementById('ws-status').textContent =
5900
+ '\u91CD\u8FDE\u4E2D\u2026 (' + state.wsRetries + '/5\uFF0C' + (wsRetryDelay/1000).toFixed(0) + 's\u540E)';
5901
+ setTimeout(() => openWs(sessionId), wsRetryDelay);
5902
+ } else if (state.wsRetries >= 5) {
5903
+ document.getElementById('ws-status').textContent = '\u8FDE\u63A5\u5931\u8D25\uFF0C\u8BF7\u5237\u65B0\u9875\u9762';
5806
5904
  }
5807
5905
  };
5808
5906
  }
@@ -5911,6 +6009,7 @@ function generateAdminHtml(version2) {
5911
6009
  document.getElementById('chat-title').textContent =
5912
6010
  session ? (session.title || id.slice(0, 12) + '\u2026') : id;
5913
6011
  clearMessages();
6012
+ loadMessages(id);
5914
6013
  renderSidebar();
5915
6014
  setStatus('idle');
5916
6015
  connectWs(id);
@@ -5979,6 +6078,108 @@ function generateAdminHtml(version2) {
5979
6078
  document.getElementById('sidebar').classList.toggle('open');
5980
6079
  });
5981
6080
 
6081
+ // \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
6082
+ document.getElementById('config-btn').addEventListener('click', async () => {
6083
+ try {
6084
+ const data = await apiFetch('/api/config');
6085
+ document.getElementById('cfg-model').value = data.model || '';
6086
+ document.getElementById('cfg-baseurl').value = data.baseUrl || '';
6087
+ document.getElementById('cfg-logdir').value = data.logDir || '';
6088
+ document.getElementById('cfg-systemprompt').value = data.systemPrompt || '';
6089
+ } catch (e) {
6090
+ // open modal even if fetch fails; fields will be empty
6091
+ }
6092
+ document.getElementById('config-modal').classList.add('open');
6093
+ });
6094
+
6095
+ document.getElementById('cancel-config').addEventListener('click', () => {
6096
+ document.getElementById('config-modal').classList.remove('open');
6097
+ });
6098
+
6099
+ document.getElementById('save-config').addEventListener('click', async () => {
6100
+ const body = {
6101
+ model: document.getElementById('cfg-model').value,
6102
+ baseUrl: document.getElementById('cfg-baseurl').value,
6103
+ logDir: document.getElementById('cfg-logdir').value,
6104
+ systemPrompt: document.getElementById('cfg-systemprompt').value,
6105
+ };
6106
+ const apiKeyVal = document.getElementById('cfg-apikey').value;
6107
+ if (apiKeyVal) body.apiKey = apiKeyVal;
6108
+ try {
6109
+ await apiFetch('/api/config', { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) });
6110
+ } catch (e) {
6111
+ // ignore errors silently for now
6112
+ }
6113
+ document.getElementById('config-modal').classList.remove('open');
6114
+ });
6115
+
6116
+ // \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
6117
+ document.getElementById('upgrade-btn').addEventListener('click', async () => {
6118
+ const modal = document.getElementById('upgrade-modal');
6119
+ const statusEl = document.getElementById('upgrade-status');
6120
+ const outputEl = document.getElementById('upgrade-output');
6121
+ const confirmBtn = document.getElementById('confirm-upgrade');
6122
+ outputEl.style.display = 'none';
6123
+ outputEl.textContent = '';
6124
+ confirmBtn.disabled = true;
6125
+ statusEl.textContent = '\u6B63\u5728\u68C0\u67E5\u7248\u672C\u2026';
6126
+ modal.classList.add('open');
6127
+ try {
6128
+ const r = await fetch(apiUrl('/api/version/check'));
6129
+ const v = await r.json();
6130
+ if (v.needsUpdate) {
6131
+ statusEl.textContent = '\u5F53\u524D v' + v.current + '\uFF0C\u6700\u65B0 v' + v.latest + '\uFF0C\u786E\u8BA4\u5347\u7EA7\uFF1F';
6132
+ confirmBtn.disabled = false;
6133
+ } else {
6134
+ statusEl.textContent = '\u5DF2\u662F\u6700\u65B0\u7248\u672C v' + v.current;
6135
+ }
6136
+ } catch(e) {
6137
+ statusEl.textContent = '\u7248\u672C\u68C0\u67E5\u5931\u8D25\uFF1A' + e.message;
6138
+ }
6139
+ });
6140
+
6141
+ document.getElementById('cancel-upgrade').addEventListener('click', () => {
6142
+ document.getElementById('upgrade-modal').classList.remove('open');
6143
+ });
6144
+
6145
+ document.getElementById('confirm-upgrade').addEventListener('click', async () => {
6146
+ const outputEl = document.getElementById('upgrade-output');
6147
+ const confirmBtn = document.getElementById('confirm-upgrade');
6148
+ const statusEl = document.getElementById('upgrade-status');
6149
+ confirmBtn.disabled = true;
6150
+ outputEl.style.display = 'block';
6151
+ outputEl.textContent = '';
6152
+ statusEl.textContent = '\u5347\u7EA7\u4E2D\u2026';
6153
+ try {
6154
+ const r = await fetch(apiUrl('/api/system/upgrade'), { method: 'POST' });
6155
+ const reader = r.body.getReader();
6156
+ const decoder = new TextDecoder();
6157
+ while (true) {
6158
+ const { done, value } = await reader.read();
6159
+ if (done) break;
6160
+ outputEl.textContent += decoder.decode(value);
6161
+ outputEl.scrollTop = outputEl.scrollHeight;
6162
+ }
6163
+ statusEl.textContent = '\u5347\u7EA7\u5B8C\u6210\uFF0C\u8BF7\u91CD\u542F ecode web';
6164
+ } catch(e) {
6165
+ statusEl.textContent = '\u5347\u7EA7\u5931\u8D25\uFF1A' + e.message;
6166
+ }
6167
+ });
6168
+
6169
+ // \u2500\u2500 Mobile keyboard adaptation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6170
+ document.getElementById('msg-input').addEventListener('focus', () => {
6171
+ setTimeout(() => {
6172
+ document.getElementById('msg-input').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
6173
+ }, 300);
6174
+ });
6175
+
6176
+ // \u2500\u2500 Global error boundary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
6177
+ window.onerror = function(msg, src, line) {
6178
+ const wsStatusEl = document.getElementById('ws-status');
6179
+ if (wsStatusEl) wsStatusEl.textContent = '\u9519\u8BEF: ' + msg;
6180
+ return false;
6181
+ };
6182
+
5982
6183
  // \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
5983
6184
  loadSessions();
5984
6185
  </script>
@@ -6279,6 +6480,42 @@ async function sessionHubRoutes(app, opts) {
6279
6480
  );
6280
6481
  }
6281
6482
 
6483
+ // src/web/routes/system.ts
6484
+ import { execSync } from "child_process";
6485
+ import { spawn } from "child_process";
6486
+ async function systemRoutes(app, opts) {
6487
+ app.get("/api/version/check", async (_request, _reply) => {
6488
+ const current = opts.version;
6489
+ let latest;
6490
+ try {
6491
+ latest = execSync("npm view @zhongqian97-code/ecode version", {
6492
+ encoding: "utf-8",
6493
+ timeout: 2e3
6494
+ }).trim();
6495
+ } catch {
6496
+ latest = "unknown";
6497
+ }
6498
+ const needsUpdate = latest !== "unknown" && latest !== current;
6499
+ return { current, latest, needsUpdate };
6500
+ });
6501
+ app.post("/api/system/upgrade", (_request, reply) => {
6502
+ reply.raw.setHeader("Content-Type", "text/plain");
6503
+ reply.raw.write("Upgrading @zhongqian97-code/ecode...\n");
6504
+ const child = spawn("npm", ["update", "-g", "@zhongqian97-code/ecode"]);
6505
+ child.stdout.on("data", (chunk) => {
6506
+ reply.raw.write(chunk);
6507
+ });
6508
+ child.stderr.on("data", (chunk) => {
6509
+ reply.raw.write(chunk);
6510
+ });
6511
+ child.on("close", () => {
6512
+ reply.raw.write("Upgrade complete. Please restart ecode web.\n");
6513
+ reply.raw.end();
6514
+ });
6515
+ return reply;
6516
+ });
6517
+ }
6518
+
6282
6519
  // src/web/server.ts
6283
6520
  async function buildServer(opts) {
6284
6521
  const app = Fastify({ logger: false });
@@ -6306,6 +6543,7 @@ async function buildServer(opts) {
6306
6543
  await app.register(configRoutes, { config: opts.config });
6307
6544
  await app.register(automationRoutes, { config: opts.config });
6308
6545
  await app.register(chatRoutes, { config: opts.config, manager: opts.manager });
6546
+ await app.register(systemRoutes, { version: opts.version });
6309
6547
  await app.register(sessionHubRoutes, { manager: opts.manager });
6310
6548
  return app;
6311
6549
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhongqian97-code/ecode",
3
- "version": "0.5.20",
3
+ "version": "0.5.22",
4
4
  "description": "A minimal Claude Code clone with REPL interface and bash tool calling",
5
5
  "type": "module",
6
6
  "author": "zhongqian97-code",