@wendongfly/myhi 1.3.190 → 1.3.191

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.html CHANGED
@@ -11,6 +11,7 @@
11
11
  <link rel="apple-touch-icon" href="/icon.png">
12
12
  <title>myhi</title>
13
13
  <script src="/lib/machine.js"></script>
14
+ <script src="/lib/prefs.js"></script>
14
15
  <script src="/sio-client/socket.io.min.js"></script>
15
16
  <script>
16
17
  (function(){try{var t=localStorage.getItem('myhi_theme')||'dark',f=localStorage.getItem('myhi_font')||'medium',r=document.documentElement;if(t&&t!=='dark')r.setAttribute('data-theme',t);r.setAttribute('data-font',f);}catch(e){}})();
@@ -98,6 +99,8 @@
98
99
  width: auto; white-space: nowrap; flex-shrink: 0;
99
100
  }
100
101
  .hd-btn:hover { color: var(--text); border-color: var(--border2); }
102
+ #settings-btn { position: relative; }
103
+ #settings-btn.has-dot::after { content: ''; position: absolute; top: -3px; right: -3px; width: 8px; height: 8px; border-radius: 50%; background: var(--red); box-shadow: 0 0 0 2px var(--bg); }
101
104
  #user-name { display: inline-block; max-width: 7rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; margin-left: 0.2rem; }
102
105
  /* 用户菜单:顶栏 👤 点开,修改密码 / 退出登录 */
103
106
  #user-menu { display: none; position: fixed; z-index: 40; min-width: 11rem; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 8px 28px rgba(0,0,0,0.4); padding: 0.3rem; }
@@ -159,6 +162,16 @@
159
162
  .set-link:hover { background: var(--surface); }
160
163
  .set-link small { color: var(--muted); font-size: 0.7rem; }
161
164
  .set-link::after { content: '›'; margin-left: auto; color: var(--muted); font-size: 1rem; }
165
+ .ai-banner { margin: 0.75rem 0.75rem 0; padding: 0.7rem 0.85rem; border-radius: 10px; border: 1px solid rgba(210,153,34,0.45); background: rgba(210,153,34,0.1); font-size: 0.82rem; line-height: 1.6; color: var(--text); }
166
+ .ai-banner b { display: block; margin-bottom: 0.15rem; }
167
+ .ai-banner code { background: var(--bg); padding: 0.05rem 0.35rem; border-radius: 4px; font-size: 0.78rem; }
168
+ .ai-banner button { margin-top: 0.45rem; background: var(--accent); color: #fff; border: none; border-radius: 8px; padding: 0.42rem 0.9rem; font-size: 0.8rem; font-weight: 600; cursor: pointer; }
169
+ .ai-banner button:disabled { opacity: 0.6; cursor: default; }
170
+ .ai-banner.ok { border-color: rgba(63,185,80,0.45); background: rgba(63,185,80,0.1); }
171
+ .ai-banner.err { border-color: rgba(248,81,73,0.45); background: rgba(248,81,73,0.08); }
172
+ /* 当前使用模式(单用户 / 多用户·管理员 / 多用户·成员),让成员知道为什么有些项是只读 */
173
+ .set-mode { font-size: 0.72rem; color: var(--muted); margin: 0.35rem 0.25rem 0; }
174
+ .set-mode a { color: var(--accent); text-decoration: none; }
162
175
  .set-foot { font-size: 0.7rem; color: var(--muted); line-height: 1.5; margin: 0.9rem 0.25rem 0.2rem; text-align: center; }
163
176
  /* 手机(≤540px):藏用户名、文字标签只留图标,一行放下且不憋屈 */
164
177
  @media (max-width: 540px) {
@@ -611,6 +624,8 @@
611
624
  </div>
612
625
 
613
626
  <!-- 升级管理已移至 /admin -->
627
+ <!-- AI 引擎(Claude Code)没装时的提示:npm 装 myhi 不会顺带装它,新机器第一次用最容易卡在这 -->
628
+ <div id="ai-banner" class="ai-banner" style="display:none"></div>
614
629
 
615
630
  <div id="view-controls" style="display:flex;flex-direction:column;gap:0.5rem;padding:0.75rem 0.75rem 0">
616
631
  <!-- 会话搜索:任务多了光看标题认不出,连首条消息/目录/标签一起搜 -->
@@ -718,6 +733,7 @@
718
733
  <div class="sheet-handle"></div>
719
734
  <div class="set-head"><b>设置</b><button class="set-x" onclick="closeSettings()" title="关闭 (Esc)">✕</button></div>
720
735
  </div>
736
+ <div id="set-mode" class="set-mode"></div>
721
737
 
722
738
  <div class="set-group-t">本设备</div>
723
739
  <div class="set-card">
@@ -745,12 +761,19 @@
745
761
  <div id="set-asr-status" class="set-status"></div>
746
762
  </div>
747
763
 
748
- <div class="set-group-t">服务</div>
749
- <div class="set-card">
750
- <a id="set-admin-link" class="set-link" href="/admin" style="display:none">🛠 管理后台 <small>用户 · 网关 · 密钥 · 升级</small></a>
751
- <button class="set-link" onclick="restartMyhi()">🔄 重启 myhi</button>
764
+ <div id="set-svc">
765
+ <div class="set-group-t" id="set-svc-t">服务</div>
766
+ <div class="set-card">
767
+ <div class="set-item">
768
+ <div class="set-lbl">myhi 版本<small id="set-ver-sub">检查中…</small></div>
769
+ <button id="set-up-btn" class="set-btn dirty" style="display:none" onclick="homeUpgrade()">升级</button>
770
+ </div>
771
+ <div id="set-up-status" class="set-status"></div>
772
+ <a id="set-admin-link" class="set-link admin-only" href="/admin">🛠 管理后台 <small>用户 · 网关 · 密钥 · Claude 升级</small></a>
773
+ <button class="set-link" onclick="restartMyhi()">🔄 重启 myhi</button>
774
+ </div>
775
+ <div id="set-restart-status" class="set-status" style="padding:0.45rem 0.25rem 0;margin:0"></div>
752
776
  </div>
753
- <div id="set-restart-status" class="set-status" style="padding:0.45rem 0.25rem 0;margin:0"></div>
754
777
  <div class="set-foot">聊天显示、回车键、快捷栏在会话页右上角 ⚙ 设置</div>
755
778
  </div>
756
779
 
@@ -877,6 +900,7 @@
877
900
 
878
901
  // 加载当前用户信息(独占模式显示用户名、退出按钮、锁定目录)
879
902
  let _userDir = null;
903
+ let _isAdmin = true;
880
904
  fetch('/api/me').then(r => r.json()).then(data => {
881
905
  if ((data.exclusive || data.hasUsers) && data.name) {
882
906
  document.getElementById('user-name').textContent = data.name;
@@ -885,8 +909,14 @@
885
909
  fitHeader();
886
910
  }
887
911
  // 成员用户(users.json 登录)才显示自助改密入口;管理员改密码在 /admin
888
- if (data.role === 'admin') document.getElementById('set-admin-link').style.display = '';
889
- else document.getElementById('set-admin-link').remove(); // 别留个隐藏首项,免得下一项顶部多一道分隔线
912
+ // 改全局配置只给管理员(后端同样拦):成员的「服务」组没有管理后台入口,本机显示名与语音只读
913
+ _isAdmin = (data.role || 'admin') === 'admin';
914
+ document.getElementById('set-mode').innerHTML = !data.hasUsers ? '👤 单用户模式 · 所有功能都可用'
915
+ : _isAdmin ? '👥 多用户模式 · 你是管理员 · <a href="/admin#mode">管理成员 →</a>'
916
+ : `👥 多用户模式 · 你是成员「${esc(data.name || '')}」· 本机显示名、语音等全局设置需管理员修改`;
917
+ if (!_isAdmin) document.querySelectorAll('#set-svc .admin-only').forEach(el => el.remove());
918
+ checkAiEngine();
919
+ checkHomeVersion();
890
920
  if (data.hasUsers && data.role === 'operator') document.getElementById('passwd-btn').style.display = '';
891
921
  if (data.dir) {
892
922
  _userDir = data.dir;
@@ -1052,6 +1082,7 @@
1052
1082
  const b = e.target.closest('button[data-k]');
1053
1083
  if (!b) return;
1054
1084
  localStorage.setItem(b.dataset.k, b.dataset.v);
1085
+ window.myhiPrefsPush?.(); // 同步到其他设备
1055
1086
  applyAppearance(); renderAppearance();
1056
1087
  });
1057
1088
  function setStatus(id, text, color) {
@@ -1067,7 +1098,7 @@
1067
1098
 
1068
1099
  window.openSettings = function() {
1069
1100
  renderAppearance();
1070
- ['set-machine-status', 'set-asr-status', 'set-restart-status'].forEach(id => setStatus(id, ''));
1101
+ ['set-machine-status', 'set-asr-status', 'set-restart-status', 'set-up-status'].filter(id => document.getElementById(id)).forEach(id => setStatus(id, ''));
1071
1102
  document.querySelectorAll('#set-sheet .set-btn').forEach(b => b.classList.remove('dirty'));
1072
1103
  document.getElementById('set-backdrop').classList.add('open');
1073
1104
  document.getElementById('set-sheet').classList.add('open');
@@ -1075,12 +1106,17 @@
1075
1106
  const inp = document.getElementById('set-machine-name');
1076
1107
  inp.placeholder = d.hostname || '';
1077
1108
  inp.value = d.name === d.hostname ? '' : (d.name || '');
1078
- inp.disabled = !!d.fromEnv;
1109
+ inp.disabled = !!d.fromEnv || !_isAdmin;
1079
1110
  if (d.fromEnv) setStatus('set-machine-status', '已由环境变量 MYHI_NAME 指定,此处不可改');
1111
+ else if (!_isAdmin) setStatus('set-machine-status', '仅管理员可修改');
1080
1112
  }).catch(() => {});
1081
1113
  fetch('/api/server-config').then(readJson).then(d => {
1082
- document.getElementById('set-asr-url').value = d.asrUrl || '';
1114
+ const inp = document.getElementById('set-asr-url');
1115
+ inp.value = d.asrUrl || '';
1116
+ inp.disabled = !_isAdmin;
1117
+ if (!_isAdmin) setStatus('set-asr-status', '仅管理员可修改');
1083
1118
  }).catch(() => {});
1119
+ checkHomeVersion();
1084
1120
  };
1085
1121
  // 输入框改了内容 → 保存按钮亮起;回车 = 保存
1086
1122
  document.querySelectorAll('#set-sheet input[data-save]').forEach(inp => {
@@ -1125,6 +1161,94 @@
1125
1161
  } catch (e) { setStatus('set-asr-status', '保存失败:' + e.message, 'red'); }
1126
1162
  };
1127
1163
 
1164
+ // ── AI 引擎(Claude Code)检测 / 一键安装 ──
1165
+ const AI_MANUAL = '也可以在这台电脑上执行 <code>npm i -g @anthropic-ai/claude-code</code>';
1166
+ const AI_LOGIN = '还差一步:在这台电脑上打开终端,运行一次 <code>claude</code> 登录 Anthropic 账号;或者让管理员在 <a href="/admin">管理后台</a> 配置大模型网关。';
1167
+ function aiBanner(cls, html) {
1168
+ const el = document.getElementById('ai-banner');
1169
+ el.className = 'ai-banner' + (cls ? ' ' + cls : '');
1170
+ el.innerHTML = html;
1171
+ el.style.display = html ? '' : 'none';
1172
+ }
1173
+ async function checkAiEngine() {
1174
+ try {
1175
+ const d = await (await fetch('/api/claude/version', { cache: 'no-store' })).json();
1176
+ if (d.upgrade?.running) return pollAiInstall(Date.now());
1177
+ if (d.current) return aiBanner('', '');
1178
+ aiBanner('', '<b>⚠ AI 引擎还没安装</b>myhi 的 AI 任务靠 Claude Code 运行,这台电脑上还没有,发消息会失败。' +
1179
+ (_isAdmin ? '<br><button id="ai-install-btn" onclick="installAiEngine()">一键安装 AI 引擎</button><br><small>' + AI_MANUAL + '</small>'
1180
+ : '<br>请联系管理员安装。'));
1181
+ } catch {}
1182
+ }
1183
+ window.installAiEngine = async function() {
1184
+ if (!confirm('在这台电脑上安装 Claude Code(myhi 的 AI 引擎)?\n下载约 200MB,需要几分钟,期间可以继续用其他功能。')) return;
1185
+ try {
1186
+ const r = await fetch('/api/claude/update', { method: 'POST' });
1187
+ const d = await r.json().catch(() => ({}));
1188
+ if (!d.ok) return aiBanner('err', '<b>安装没有开始</b>' + esc(d.error || ('HTTP ' + r.status)) + '<br><small>' + AI_MANUAL + '</small>');
1189
+ pollAiInstall(Date.now());
1190
+ } catch (e) { aiBanner('err', '<b>安装没有开始</b>' + esc(e.message)); }
1191
+ };
1192
+ async function pollAiInstall(t0) {
1193
+ aiBanner('', `<b>⏳ 正在安装 AI 引擎…</b>已用 ${Math.round((Date.now() - t0) / 1000)} 秒,装好后这里会提示下一步。`);
1194
+ let s = null;
1195
+ try { s = await (await fetch('/api/claude/update-status', { cache: 'no-store' })).json(); } catch {}
1196
+ if (!s || s.running) return setTimeout(() => pollAiInstall(t0), 4000);
1197
+ if (s.ok) aiBanner('ok', `<b>✓ AI 引擎已安装(v${esc(s.version || '')})</b>` + AI_LOGIN);
1198
+ else aiBanner('err', '<b>安装失败</b><code style="white-space:pre-wrap;display:block;margin:0.3rem 0">' + esc(s.error || '') + '</code>' + AI_MANUAL);
1199
+ }
1200
+
1201
+ // ── 版本 / 升级(成员也可升级):有新版时 ⚙ 按钮挂小红点,设置里一键升级 ──
1202
+ let _homeVer = null;
1203
+ async function checkHomeVersion() {
1204
+ try {
1205
+ const r = await fetch('/api/version', { cache: 'no-store' });
1206
+ _homeVer = await r.json();
1207
+ const sub = document.getElementById('set-ver-sub'), btn = document.getElementById('set-up-btn');
1208
+ if (!sub) return;
1209
+ sub.textContent = `当前 v${_homeVer.current}` + (_homeVer.updateAvailable ? ` · 有新版 v${_homeVer.latest}` : ' · 已是最新');
1210
+ btn.style.display = _homeVer.updateAvailable ? '' : 'none';
1211
+ btn.textContent = '升级到 v' + _homeVer.latest;
1212
+ document.getElementById('settings-btn').classList.toggle('has-dot', !!_homeVer.updateAvailable);
1213
+ } catch {}
1214
+ }
1215
+ // 等升级落地:轮询 /api/version(重启/停服期间连不上是正常的)。成功 = 版本变了(且等于目标);
1216
+ // 失败 = 服务端报了本次升级的失败原因;10 分钟兜底超时。(会话页 / 管理后台同款)
1217
+ async function waitUpgrade(from, target, onTick) {
1218
+ const t0 = Date.now();
1219
+ while (Date.now() - t0 < 10 * 60 * 1000) {
1220
+ await new Promise(r => setTimeout(r, 3000));
1221
+ let v = null;
1222
+ try { const r = await fetch('/api/version', { cache: 'no-store' }); if (r.ok) v = await r.json(); } catch {}
1223
+ onTick?.(!!v, Math.round((Date.now() - t0) / 1000));
1224
+ if (!v) continue;
1225
+ if (v.current !== from && (!target || v.current === target)) return { ok: true, version: v.current };
1226
+ const last = v.upgrade?.last;
1227
+ if (!v.upgrade?.running && last && !last.ok && last.at >= t0 - 5000) return { ok: false, error: last.error };
1228
+ }
1229
+ return { ok: false, error: '等了 10 分钟仍未完成,请稍后刷新页面查看版本,或到服务器上查看 myhi log' };
1230
+ }
1231
+ window.homeUpgrade = async function(force) {
1232
+ if (!_homeVer?.updateAvailable) return;
1233
+ if (!force && !confirm(`升级 myhi?\n当前 v${_homeVer.current} → 最新 v${_homeVer.latest}\n\n升级后服务会自动重启,期间页面会短暂断开。`)) return;
1234
+ const btn = document.getElementById('set-up-btn');
1235
+ btn.disabled = true;
1236
+ setStatus('set-up-status', '正在提交升级…');
1237
+ try {
1238
+ const r = await fetch('/api/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ force: !!force }) });
1239
+ const d = await r.json().catch(() => ({}));
1240
+ if (!d.ok) {
1241
+ btn.disabled = false;
1242
+ if (d.busy?.length && confirm(d.error + '\n\n仍然现在升级吗?')) return homeUpgrade(true);
1243
+ return setStatus('set-up-status', '升级失败:' + (d.error || ('HTTP ' + r.status)), 'red');
1244
+ }
1245
+ setStatus('set-up-status', d.message || '升级中…');
1246
+ const res = await waitUpgrade(d.from, d.target, (up, sec) => setStatus('set-up-status', (up ? '正在安装新版本… ' : '服务重启中… ') + sec + 's'));
1247
+ if (res.ok) { setStatus('set-up-status', `✓ 已升级到 v${res.version},即将刷新`, 'green'); setTimeout(() => location.reload(), 800); }
1248
+ else { btn.disabled = false; setStatus('set-up-status', '升级失败:' + res.error, 'red'); }
1249
+ } catch (e) { btn.disabled = false; setStatus('set-up-status', '升级失败:' + e.message, 'red'); }
1250
+ };
1251
+
1128
1252
  window.restartMyhi = async function() {
1129
1253
  if (!confirm('确认重启 myhi 服务?\n重启期间连接会短暂中断,页面将自动重连。')) return;
1130
1254
  try { await fetch('/api/restart', { method: 'POST' }); } catch {}