@wendongfly/myhi 1.3.180 → 1.3.182

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/chat.html CHANGED
@@ -410,8 +410,6 @@
410
410
  .dr-text { color: var(--text); font-size: 0.82rem; line-height: 1.5; word-break: break-word; white-space: pre-wrap; }
411
411
  .dr-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem 0.6rem; margin-top: 0.35rem; font-size: 0.66rem; color: var(--muted); }
412
412
  .dr-acts { display: flex; gap: 0.4rem; margin-left: auto; flex-shrink: 0; }
413
- .atpick-row { padding: 0.42rem 0.7rem; cursor: pointer; color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
414
- .atpick-row:hover { background: var(--surface2); color: var(--text); }
415
413
  .dr-btn { flex-shrink: 0; white-space: nowrap; padding: 0.28rem 0.7rem; font-size: 0.72rem; border: none; border-radius: 6px; background: var(--surface2); color: var(--text2); cursor: pointer; }
416
414
  .dr-btn.take { background: var(--green); color: #fff; font-weight: 600; }
417
415
  .dr-btn:active { opacity: 0.75; }
@@ -665,48 +663,9 @@
665
663
  <div id="sk-visible-list" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0.15rem 0.6rem;margin-top:0.5rem"></div>
666
664
  </div>
667
665
  <div class="setting-group" data-stab="more" style="display:none">
668
- <h4>本机名称</h4>
669
- <div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.35rem;line-height:1.5">多台电脑都跑 myhi 时用来区分:浏览器标题以它开头,标签图标按名字自动配色。留空 = 用电脑主机名。</div>
670
- <div style="display:flex;gap:0.4rem">
671
- <input id="set-machine-name" class="slash-inp" maxlength="32" autocomplete="off" style="flex:1;font-size:0.82rem">
672
- <button onclick="saveMachineName()" style="background:var(--blue2);color:#fff;border:none;border-radius:8px;padding:0.4rem 0.8rem;font-size:0.78rem;cursor:pointer;white-space:nowrap">保存</button>
673
- </div>
674
- <div id="set-machine-status" style="font-size:0.72rem;min-height:1.2em;margin-top:0.25rem;color:var(--muted)"></div>
675
- </div>
676
- <div class="setting-group" data-stab="more" style="display:none">
677
- <h4>@ 引用范围(项目外)</h4>
678
- <div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.35rem;line-height:1.5">除了任务目录,再把常用的资料文件夹(如公共资料库)加进来,打 @ 时也能搜到里面的文件。</div>
679
- <div id="atroots-list"></div>
680
- <button id="atpick-open" onclick="openAtPicker()" style="width:100%;margin-top:0.4rem;background:var(--blue2);color:#fff;border:none;border-radius:8px;padding:0.5rem;font-size:0.8rem;cursor:pointer">📁 选择文件夹添加…</button>
681
- <div id="atpick" style="display:none;margin-top:0.4rem;border:1px solid var(--border);border-radius:10px;overflow:hidden">
682
- <div id="atpick-drives" style="display:none;gap:0.3rem;padding:0.4rem 0.5rem 0;flex-wrap:wrap"></div>
683
- <div style="display:flex;align-items:center;gap:0.35rem;padding:0.4rem 0.5rem;border-bottom:1px solid var(--border)">
684
- <button id="atpick-up" class="dr-btn" onclick="atPickUp()" title="上一级">⬆ 上一级</button>
685
- <span id="atpick-cur" style="flex:1;min-width:0;font-size:0.72rem;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left"></span>
686
- </div>
687
- <div id="atpick-list" style="max-height:200px;overflow-y:auto;scrollbar-width:thin;font-size:0.8rem"></div>
688
- <div style="display:flex;gap:0.4rem;padding:0.45rem 0.5rem;border-top:1px solid var(--border)">
689
- <button class="dr-btn" onclick="closeAtPicker()" style="flex:1;padding:0.45rem">取消</button>
690
- <button onclick="addAtPicked()" style="flex:2;background:var(--blue2);color:#fff;border:none;border-radius:6px;padding:0.45rem;font-size:0.78rem;cursor:pointer">✓ 添加当前文件夹</button>
691
- </div>
692
- </div>
693
- <details style="margin-top:0.35rem;font-size:0.72rem;color:var(--muted)">
694
- <summary style="cursor:pointer">手动输入路径</summary>
695
- <div style="display:flex;gap:0.4rem;margin-top:0.35rem">
696
- <input id="atroot-new" class="slash-inp" placeholder="例如 D:\资料库" autocomplete="off" style="flex:1;font-size:0.82rem">
697
- <button onclick="addAtRoot()" style="background:var(--blue2);color:#fff;border:none;border-radius:8px;padding:0.4rem 0.8rem;font-size:0.78rem;cursor:pointer;white-space:nowrap">添加</button>
698
- </div>
699
- </details>
700
- <div id="atroot-status" style="font-size:0.72rem;min-height:1.2em;margin-top:0.25rem;color:var(--muted)"></div>
701
- </div>
702
- <div class="setting-group" data-stab="more" style="display:none">
703
- <h4>语音识别 (ASR)</h4>
704
- <div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.35rem">SenseVoice 或兼容服务的地址,留空表示禁用语音</div>
705
- <div style="display:flex;gap:0.4rem">
706
- <input id="set-asr-url" class="slash-inp" placeholder="http://192.168.x.x:8765" autocomplete="off" style="flex:1;font-size:0.82rem">
707
- <button onclick="saveAsrUrl()" style="background:var(--blue2);color:#fff;border:none;border-radius:8px;padding:0.4rem 0.8rem;font-size:0.78rem;cursor:pointer;white-space:nowrap">保存</button>
708
- </div>
709
- <div id="set-asr-status" style="font-size:0.72rem;min-height:1.2em;margin-top:0.25rem;color:var(--muted)"></div>
666
+ <h4>全局设置</h4>
667
+ <div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.45rem;line-height:1.5">本机名称、@ 引用范围、语音识别、账户等对所有任务生效的设置,统一放在首页的 ⚙ 设置里。这里的「外观 / 聊天 / 快捷栏」只影响当前这台设备。</div>
668
+ <button onclick="location.href='/?settings=1'" style="width:100%;background:var(--blue2);color:#fff;border:none;border-radius:8px;padding:0.55rem;font-size:0.82rem;cursor:pointer">前往首页 ⚙ 设置 →</button>
710
669
  </div>
711
670
  <div class="setting-group" data-stab="more" style="display:none">
712
671
  <h4>服务管理</h4>
@@ -1334,93 +1293,6 @@
1334
1293
  updateEnterHint();
1335
1294
  syncSkModeUI();
1336
1295
 
1337
- // ── @ 引用范围(项目外目录):客户自助配置,@ 补全可引用这些目录 ──
1338
- let _atRoots = null;
1339
- function renderAtRoots() {
1340
- const el = document.getElementById('atroots-list');
1341
- if (!el) return;
1342
- el.innerHTML = (_atRoots || []).length
1343
- ? _atRoots.map((r, i) => `<div style="display:flex;align-items:center;gap:0.4rem;padding:0.25rem 0;font-size:0.78rem"><span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtml(r)}">📁 ${escHtml(r)}</span><button class="dr-btn" onclick="delAtRoot(${i})">删</button></div>`).join('')
1344
- : '<div style="font-size:0.72rem;color:var(--muted)">(未配置——@ 只搜任务目录)</div>';
1345
- }
1346
- function loadAtRoots() {
1347
- return fetch('/api/atroots').then(r => r.json()).then(d => { _atRoots = d.roots || []; renderAtRoots(); }).catch(() => {});
1348
- }
1349
- async function saveAtRoots(roots) {
1350
- const st = document.getElementById('atroot-status');
1351
- try {
1352
- const r = await fetch('/api/atroots', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ roots }) });
1353
- const d = await r.json();
1354
- if (!d.ok) { st.textContent = '保存失败:' + (d.error || ''); return false; }
1355
- _atRoots = d.roots; renderAtRoots(); st.textContent = '已保存';
1356
- return true;
1357
- } catch (e) { st.textContent = '保存出错:' + e.message; return false; }
1358
- }
1359
- window.addAtRoot = function() {
1360
- const inp = document.getElementById('atroot-new');
1361
- const v = inp.value.trim();
1362
- if (!v) return;
1363
- saveAtRoots([...(_atRoots || []), v]).then(ok => { if (ok) inp.value = ''; });
1364
- };
1365
- window.delAtRoot = function(i) { saveAtRoots((_atRoots || []).filter((_, j) => j !== i)); };
1366
-
1367
- // 文件夹点选(复用 /api/dirs 目录浏览):普通用户不会敲绝对路径,点进去选中即可
1368
- let _atPick = { current: '', parent: null };
1369
- window.openAtPicker = function() {
1370
- document.getElementById('atpick').style.display = '';
1371
- document.getElementById('atpick-open').style.display = 'none';
1372
- atPickGo('');
1373
- };
1374
- window.closeAtPicker = function() {
1375
- document.getElementById('atpick').style.display = 'none';
1376
- document.getElementById('atpick-open').style.display = '';
1377
- };
1378
- window.atPickGo = async function(path) {
1379
- const list = document.getElementById('atpick-list');
1380
- list.innerHTML = '<div style="padding:0.6rem;color:var(--muted);font-size:0.75rem">读取中…</div>';
1381
- try {
1382
- const r = await fetch('/api/dirs' + (path ? '?path=' + encodeURIComponent(path) : ''));
1383
- const d = await r.json();
1384
- if (!r.ok) throw new Error(d.error || r.status);
1385
- _atPick = d;
1386
- const cur = document.getElementById('atpick-cur');
1387
- cur.textContent = '\u200e' + d.current; // rtl 截断时保留路径尾部;LRM 防标点跑位
1388
- cur.title = d.current;
1389
- document.getElementById('atpick-up').disabled = !d.parent;
1390
- document.getElementById('atpick-up').style.opacity = d.parent ? '' : '0.4';
1391
- const dv = document.getElementById('atpick-drives');
1392
- dv.style.display = (d.drives || []).length ? 'flex' : 'none';
1393
- dv.innerHTML = '';
1394
- (d.drives || []).forEach(dr => {
1395
- const b = document.createElement('button');
1396
- b.className = 'dr-btn';
1397
- b.textContent = '💽 ' + dr.replace(/\\$/, '');
1398
- if (d.current.toUpperCase().startsWith(dr.toUpperCase())) b.style.cssText = 'background:var(--blue2);color:#fff';
1399
- b.onclick = () => atPickGo(dr);
1400
- dv.appendChild(b);
1401
- });
1402
- list.innerHTML = '';
1403
- if (!d.dirs.length) list.innerHTML = '<div style="padding:0.6rem;color:var(--muted);font-size:0.75rem">(没有子文件夹)</div>';
1404
- d.dirs.forEach(x => {
1405
- const row = document.createElement('div');
1406
- row.className = 'atpick-row';
1407
- row.textContent = '📁 ' + x.name;
1408
- row.onclick = () => atPickGo(x.path);
1409
- list.appendChild(row);
1410
- });
1411
- } catch (e) {
1412
- list.innerHTML = '<div style="padding:0.6rem;color:var(--red);font-size:0.75rem">读取失败:' + escHtml(String(e.message || e)) +
1413
- (String(e.message).includes('权限') ? '(仅管理员可浏览目录,可展开下方「手动输入路径」)' : '') + '</div>';
1414
- }
1415
- };
1416
- window.atPickUp = function() { if (_atPick.parent) atPickGo(_atPick.parent); };
1417
- window.addAtPicked = async function() {
1418
- const p = _atPick.current;
1419
- if (!p) return;
1420
- if ((_atRoots || []).includes(p)) { document.getElementById('atroot-status').textContent = '这个文件夹已经在列表里了'; return; }
1421
- if (await saveAtRoots([...(_atRoots || []), p])) closeAtPicker();
1422
- };
1423
-
1424
1296
  // 输出累积
1425
1297
  let outputBuffer = '';
1426
1298
  let outputTimer = null;
@@ -2415,6 +2287,15 @@
2415
2287
  scrollToBottom();
2416
2288
  }
2417
2289
 
2290
+ // 解析接口 JSON。升级后没重启 server 时,新页面调新接口会拿到 Express 的 404 HTML,
2291
+ // 直接 .json() 只报「Unexpected token '<'」,用户看不懂 → 换成明确的处理建议
2292
+ async function readApiJson(resp) {
2293
+ const text = await resp.text();
2294
+ try { return JSON.parse(text); } catch {}
2295
+ if (resp.status === 404 || /^\s*</.test(text)) throw new Error('服务端还是旧版本——升级后需要重启 myhi(设置→更多→重启 myhi)');
2296
+ throw new Error(`服务端返回异常(HTTP ${resp.status})`);
2297
+ }
2298
+
2418
2299
  // 复制到剪贴板。HTTP 访问(非安全上下文)没有 navigator.clipboard,退回 textarea + execCommand
2419
2300
  async function copyText(text) {
2420
2301
  try {
@@ -3526,7 +3407,7 @@
3526
3407
  overlay.appendChild(wrap);
3527
3408
  };
3528
3409
 
3529
- window.openSettings = function() { document.getElementById('settings-backdrop').classList.add('open'); document.getElementById('settings-panel').classList.add('open'); syncSettingsUI(); loadAtRoots(); applySettingsTab(localStorage.getItem('myhi_settings_tab') || 'look'); };
3410
+ window.openSettings = function() { document.getElementById('settings-backdrop').classList.add('open'); document.getElementById('settings-panel').classList.add('open'); syncSettingsUI(); applySettingsTab(localStorage.getItem('myhi_settings_tab') || 'look'); };
3530
3411
  // 设置抽屉分类页签(1.3.174:一屏只看一类,不再长条堆叠)
3531
3412
  function applySettingsTab(tab) {
3532
3413
  document.querySelectorAll('#settings-tabs .stab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
@@ -4068,17 +3949,6 @@
4068
3949
  document.getElementById('set-rawclip').checked = settings.rawClip !== false;
4069
3950
  syncEnterModeUI();
4070
3951
  syncSkModeUI();
4071
- // 从服务端加载 ASR URL
4072
- fetch('/api/server-config').then(r => r.json()).then(d => {
4073
- document.getElementById('set-asr-url').value = d.asrUrl || '';
4074
- }).catch(() => {});
4075
- fetch('/api/machine-name').then(r => r.json()).then(d => {
4076
- const inp = document.getElementById('set-machine-name');
4077
- inp.placeholder = d.hostname || '';
4078
- inp.value = d.name === d.hostname ? '' : (d.name || '');
4079
- inp.disabled = !!d.fromEnv;
4080
- document.getElementById('set-machine-status').textContent = d.fromEnv ? '已由环境变量 MYHI_NAME 指定,此处不可改' : '';
4081
- }).catch(() => {});
4082
3952
  }
4083
3953
  document.getElementById('set-parse').addEventListener('change', (e) => { settings.parseOutput = e.target.checked; saveSettings(); });
4084
3954
  document.getElementById('set-thinking').addEventListener('change', (e) => { settings.showThinking = e.target.checked; saveSettings(); });
@@ -4090,50 +3960,6 @@
4090
3960
  chatArea.querySelectorAll('.msg-raw').forEach(m => { delete m.dataset.open; rawClipSync(m); });
4091
3961
  });
4092
3962
 
4093
- window.saveMachineName = async function() {
4094
- const statusEl = document.getElementById('set-machine-status');
4095
- try {
4096
- const resp = await fetch('/api/machine-name', {
4097
- method: 'POST',
4098
- headers: { 'Content-Type': 'application/json' },
4099
- body: JSON.stringify({ name: document.getElementById('set-machine-name').value.trim() }),
4100
- });
4101
- const data = await resp.json();
4102
- if (!data.ok) throw new Error(data.error || '未知错误');
4103
- window.myhiSetMachineName?.(data.name);
4104
- statusEl.style.color = 'var(--green)';
4105
- statusEl.textContent = `已保存:${data.name}(其他已打开的页面刷新后生效)`;
4106
- } catch (e) {
4107
- statusEl.style.color = 'var(--red)';
4108
- statusEl.textContent = '保存失败: ' + e.message;
4109
- }
4110
- };
4111
-
4112
- window.saveAsrUrl = async function() {
4113
- const url = document.getElementById('set-asr-url').value.trim();
4114
- const statusEl = document.getElementById('set-asr-status');
4115
- statusEl.style.color = 'var(--muted)';
4116
- statusEl.textContent = '保存中...';
4117
- try {
4118
- const resp = await fetch('/api/server-config', {
4119
- method: 'POST',
4120
- headers: { 'Content-Type': 'application/json' },
4121
- body: JSON.stringify({ asrUrl: url }),
4122
- });
4123
- const data = await resp.json();
4124
- if (data.ok) {
4125
- statusEl.style.color = 'var(--green)';
4126
- statusEl.textContent = url ? '已保存,重启后生效(如已设置 ASR_URL 环境变量则以环境变量为准)' : '已清空';
4127
- } else {
4128
- statusEl.style.color = 'var(--red)';
4129
- statusEl.textContent = '保存失败: ' + (data.error || '未知错误');
4130
- }
4131
- } catch (e) {
4132
- statusEl.style.color = 'var(--red)';
4133
- statusEl.textContent = '请求失败: ' + e.message;
4134
- }
4135
- };
4136
-
4137
3963
  window.restartMyhi = async function() {
4138
3964
  if (!confirm('确认重启 myhi 服务?\n重启期间连接会短暂中断,页面将自动重连。')) return;
4139
3965
  try {
package/dist/index.html CHANGED
@@ -57,7 +57,8 @@
57
57
  display: flex;
58
58
  align-items: center;
59
59
  justify-content: space-between;
60
- padding: 0 1rem;
60
+ gap: 0.75rem;
61
+ padding: 0 0.75rem;
61
62
  padding-top: max(1rem, env(safe-area-inset-top));
62
63
  padding-bottom: 0.75rem;
63
64
  background: var(--bg);
@@ -96,6 +97,35 @@
96
97
  font-size: 0.75rem; padding: 0.3rem 0.6rem; border-radius: 6px; cursor: pointer;
97
98
  width: auto; white-space: nowrap; flex-shrink: 0;
98
99
  }
100
+ .hd-btn:hover { color: var(--text); border-color: var(--border2); }
101
+ #user-name { max-width: 7rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
102
+
103
+ /* ── 设置 sheet(全局设置收口:外观 / 本机名称 / @ 引用范围 / 语音 / 账户 / 服务)── */
104
+ #set-backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 30; }
105
+ #set-sheet { display: none; position: fixed; left: 0; right: 0; bottom: 0; z-index: 31; max-width: 640px; margin: 0 auto;
106
+ background: var(--surface); border-radius: 16px 16px 0 0; padding: 0 1rem calc(1rem + env(safe-area-inset-bottom)); max-height: 88vh; overflow-y: auto; }
107
+ #set-sheet.open, #set-backdrop.open { display: block; }
108
+ .set-sec { padding: 0.75rem 0; border-top: 1px solid var(--border); }
109
+ .sheet-title + .set-sec { border-top: none; padding-top: 0; }
110
+ .set-sec h4 { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.2rem; display: flex; align-items: center; gap: 0.4rem; }
111
+ .set-scope { font-size: 0.66rem; font-weight: 400; color: var(--muted); border: 1px solid var(--border2); border-radius: 4px; padding: 0 0.3rem; }
112
+ .set-hint { font-size: 0.72rem; color: var(--muted); line-height: 1.5; margin-bottom: 0.4rem; }
113
+ .set-status { font-size: 0.72rem; min-height: 1.1em; margin-top: 0.25rem; color: var(--muted); }
114
+ .set-row { display: flex; gap: 0.4rem; }
115
+ .set-inp { flex: 1; min-width: 0; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; color: var(--text); font-size: 0.82rem; padding: 0.45rem 0.6rem; outline: none; }
116
+ .set-inp:focus { border-color: var(--accent); }
117
+ .set-btn { flex-shrink: 0; white-space: nowrap; background: var(--accent); color: #fff; border: none; border-radius: 8px; padding: 0.45rem 0.85rem; font-size: 0.78rem; cursor: pointer; }
118
+ .set-btn.ghost { background: var(--bg); color: var(--text2); border: 1px solid var(--border); }
119
+ .set-btn.danger { background: transparent; color: var(--red); border: 1px solid var(--red); }
120
+ .set-seg { display: flex; gap: 0.35rem; flex-wrap: wrap; }
121
+ .set-seg button { flex: 1; min-width: 3rem; background: var(--bg); color: var(--muted); border: 1px solid var(--border); border-radius: 8px; padding: 0.45rem 0.3rem; font-size: 0.8rem; cursor: pointer; white-space: nowrap; }
122
+ .set-seg button.on { background: var(--accent); border-color: var(--accent); color: #fff; }
123
+ .set-sub { font-size: 0.75rem; color: var(--text2); margin: 0.5rem 0 0.3rem; }
124
+ .atr-row { display: flex; align-items: center; gap: 0.4rem; padding: 0.2rem 0; font-size: 0.78rem; }
125
+ .atr-row span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
126
+ .atpick-row { padding: 0.42rem 0.7rem; cursor: pointer; color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.8rem; }
127
+ .atpick-row:hover { background: var(--bg); color: var(--text); }
128
+ .dr-btn { flex-shrink: 0; white-space: nowrap; padding: 0.28rem 0.7rem; font-size: 0.72rem; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text2); cursor: pointer; }
99
129
  /* 手机(≤540px):藏用户名、文字标签只留图标,一行放下且不憋屈 */
100
130
  @media (max-width: 540px) {
101
131
  #user-name { display: none; }
@@ -522,20 +552,18 @@
522
552
  <div class="logo">my<span>hi</span></div>
523
553
  <div class="hd-actions">
524
554
  <span id="user-name" style="font-size:0.8rem;color:var(--muted)"></span>
525
- <button id="grid-btn" class="hd-btn" onclick="location.href='/grid'" title="分屏工作台(多任务并排)">▦ 分屏</button>
526
- <button id="appearance-btn" class="hd-btn" onclick="openAppearance()" title="外观">🎨</button>
527
- <button id="help-btn" class="hd-btn" onclick="openHelpSheet()" title="使用说明 / 快速上手">❓<span class="hd-txt"> 帮助</span></button>
555
+ <button id="grid-btn" class="hd-btn" onclick="location.href='/grid'" title="分屏工作台(多任务并排)">▦<span class="hd-txt"> 分屏</span></button>
528
556
  <button id="doctor-btn" class="hd-btn" onclick="openDoctor()" title="会话体检">🩺<span class="hd-txt"> 体检</span></button>
529
557
  <button id="usage-btn" class="hd-btn" onclick="showUsage()" title="用量">📊<span class="hd-txt"> 用量</span></button>
530
- <button id="passwd-btn" class="hd-btn" onclick="openPasswdSheet()" title="修改我的密码" style="display:none">🔑<span class="hd-txt"> 改密码</span></button>
531
- <button id="logout-btn" class="hd-btn" onclick="doLogout()" style="display:none">退出</button>
558
+ <button id="help-btn" class="hd-btn" onclick="openHelpSheet()" title="使用说明 / 快速上手">❓<span class="hd-txt"> 帮助</span></button>
559
+ <button id="settings-btn" class="hd-btn" onclick="openSettings()" title="设置(外观 / 本机名称 / @ 引用范围 / 语音 / 账户)">⚙<span class="hd-txt"> 设置</span></button>
532
560
  <div id="conn-dot"></div>
533
561
  </div>
534
562
  </div>
535
563
 
536
564
  <!-- 升级管理已移至 /admin -->
537
565
 
538
- <div id="view-controls" style="display:flex;flex-direction:column;gap:0.5rem;margin-bottom:0.75rem">
566
+ <div id="view-controls" style="display:flex;flex-direction:column;gap:0.5rem;padding:0.75rem 0.75rem 0">
539
567
  <!-- 会话搜索:任务多了光看标题认不出,连首条消息/目录/标签一起搜 -->
540
568
  <div style="display:flex;align-items:center;gap:0.4rem">
541
569
  <input id="sess-search" type="search" placeholder="搜任务:标题 / 首条需求 / 目录 / 标签"
@@ -625,6 +653,77 @@
625
653
  <button onclick="closeNoteSheet()" style="width:100%;background:var(--bg,var(--bg));color:var(--muted,var(--muted));border:1px solid var(--border,var(--border));border-radius:10px;padding:0.65rem;font-size:0.85rem;cursor:pointer;margin-top:0.5rem">取消</button>
626
654
  </div>
627
655
 
656
+ <!-- 设置 sheet:首页是全局设置的唯一入口(会话页 ⚙ 只管本设备的聊天显示/快捷栏) -->
657
+ <div id="set-backdrop" onclick="closeSettings()"></div>
658
+ <div id="set-sheet">
659
+ <div class="sheet-handle"></div>
660
+ <div class="sheet-title">⚙ 设置</div>
661
+ <div class="set-sec">
662
+ <h4>外观 <span class="set-scope">本设备</span></h4>
663
+ <div class="set-sub">主题</div>
664
+ <div class="set-seg" id="set-theme"></div>
665
+ <div class="set-sub">字号</div>
666
+ <div class="set-seg" id="set-font"></div>
667
+ </div>
668
+ <div class="set-sec">
669
+ <h4>本机名称 <span class="set-scope">全局</span></h4>
670
+ <div class="set-hint">多台电脑都跑 myhi 时用来区分:浏览器标题以它开头,标签图标按名字自动配色。留空 = 用电脑主机名。</div>
671
+ <div class="set-row">
672
+ <input id="set-machine-name" class="set-inp" maxlength="32" autocomplete="off">
673
+ <button class="set-btn" onclick="saveMachineName()">保存</button>
674
+ </div>
675
+ <div id="set-machine-status" class="set-status"></div>
676
+ </div>
677
+ <div class="set-sec">
678
+ <h4>@ 引用范围 <span class="set-scope">全局</span></h4>
679
+ <div class="set-hint">除了任务目录,再把常用的资料文件夹(如公共资料库)加进来,打 @ 时也能搜到里面的文件。</div>
680
+ <div id="atroots-list"></div>
681
+ <button id="atpick-open" class="set-btn" onclick="openAtPicker()" style="width:100%;margin-top:0.4rem">📁 选择文件夹添加…</button>
682
+ <div id="atpick" style="display:none;margin-top:0.4rem;border:1px solid var(--border);border-radius:10px;overflow:hidden">
683
+ <div id="atpick-drives" style="display:none;gap:0.3rem;padding:0.4rem 0.5rem 0;flex-wrap:wrap"></div>
684
+ <div style="display:flex;align-items:center;gap:0.35rem;padding:0.4rem 0.5rem;border-bottom:1px solid var(--border)">
685
+ <button id="atpick-up" class="dr-btn" onclick="atPickUp()" title="上一级">⬆ 上一级</button>
686
+ <span id="atpick-cur" style="flex:1;min-width:0;font-size:0.72rem;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left"></span>
687
+ </div>
688
+ <div id="atpick-list" style="max-height:200px;overflow-y:auto;scrollbar-width:thin"></div>
689
+ <div style="display:flex;gap:0.4rem;padding:0.45rem 0.5rem;border-top:1px solid var(--border)">
690
+ <button class="set-btn ghost" onclick="closeAtPicker()" style="flex:1">取消</button>
691
+ <button class="set-btn" onclick="addAtPicked()" style="flex:2">✓ 添加当前文件夹</button>
692
+ </div>
693
+ </div>
694
+ <details style="margin-top:0.35rem;font-size:0.72rem;color:var(--muted)">
695
+ <summary style="cursor:pointer">手动输入路径</summary>
696
+ <div class="set-row" style="margin-top:0.35rem">
697
+ <input id="atroot-new" class="set-inp" placeholder="例如 D:\资料库" autocomplete="off">
698
+ <button class="set-btn" onclick="addAtRoot()">添加</button>
699
+ </div>
700
+ </details>
701
+ <div id="atroot-status" class="set-status"></div>
702
+ </div>
703
+ <div class="set-sec">
704
+ <h4>语音识别 (ASR) <span class="set-scope">全局</span></h4>
705
+ <div class="set-hint">SenseVoice 或兼容服务的地址,留空表示禁用语音。</div>
706
+ <div class="set-row">
707
+ <input id="set-asr-url" class="set-inp" placeholder="http://192.168.x.x:8765" autocomplete="off">
708
+ <button class="set-btn" onclick="saveAsrUrl()">保存</button>
709
+ </div>
710
+ <div id="set-asr-status" class="set-status"></div>
711
+ </div>
712
+ <div class="set-sec" id="set-account" style="display:none">
713
+ <h4>账户 <span id="set-account-name" class="set-scope"></span></h4>
714
+ <div class="set-row" style="margin-top:0.4rem">
715
+ <button id="passwd-btn" class="set-btn ghost" onclick="closeSettings();openPasswdSheet()" style="flex:1;display:none">🔑 修改密码</button>
716
+ <button id="logout-btn" class="set-btn danger" onclick="doLogout()" style="flex:1;display:none">退出登录</button>
717
+ </div>
718
+ </div>
719
+ <div class="set-sec">
720
+ <h4>服务</h4>
721
+ <div class="set-hint">聊天显示、回车键、快捷栏按钮在会话页的 ⚙ 设置里调(只影响本设备)。</div>
722
+ <button class="set-btn danger" onclick="restartMyhi()" style="width:100%">重启 myhi</button>
723
+ <div id="set-restart-status" class="set-status"></div>
724
+ </div>
725
+ </div>
726
+
628
727
  <!-- 修改密码 sheet(成员用户自助改密;管理员改密码在 /admin) -->
629
728
  <div id="passwd-backdrop" onclick="closePasswdSheet()" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.55);z-index:30"></div>
630
729
  <div id="passwd-sheet" style="display:none;position:fixed;left:0;right:0;bottom:0;z-index:31;background:var(--surface);border-radius:16px 16px 0 0;padding:1rem 1rem calc(1rem + env(safe-area-inset-bottom));max-height:88vh;overflow-y:auto">
@@ -751,10 +850,13 @@
751
850
  fetch('/api/me').then(r => r.json()).then(data => {
752
851
  if ((data.exclusive || data.hasUsers) && data.name) {
753
852
  document.getElementById('user-name').textContent = data.name;
853
+ document.getElementById('set-account-name').textContent = data.name;
854
+ document.getElementById('set-account').style.display = '';
754
855
  document.getElementById('logout-btn').style.display = '';
755
856
  }
756
857
  // 成员用户(users.json 登录)才显示自助改密入口;管理员改密码在 /admin
757
858
  if (data.hasUsers && data.role === 'operator') {
859
+ document.getElementById('set-account').style.display = '';
758
860
  document.getElementById('passwd-btn').style.display = '';
759
861
  }
760
862
  if (data.dir) {
@@ -879,32 +981,177 @@
879
981
  const meta = document.querySelector('meta[name="theme-color"]');
880
982
  if (meta) meta.setAttribute('content', THEME_META[t] || '#0d1117');
881
983
  }
882
- window.openAppearance = function() {
984
+ // ── 设置 sheet ─────────────────────────────────────────
985
+ const THEMES = [['system','跟随系统'],['dark','深色'],['light','浅色'],['black','纯黑']];
986
+ const FONTS = [['small','小'],['medium','中'],['large','大']];
987
+ function renderAppearance() {
883
988
  const t = localStorage.getItem('myhi_theme') || 'dark';
884
989
  const f = localStorage.getItem('myhi_font') || 'medium';
885
- const overlay = document.createElement('div');
886
- overlay.style.cssText = 'position:fixed;inset:0;z-index:80;background:rgba(0,0,0,0.55);display:flex;align-items:flex-end;justify-content:center';
887
- const seg = (group, val, cur, label) => `<button data-g="${group}" data-v="${val}" style="flex:1;min-width:3rem;background:${val===cur?'var(--accent)':'var(--bg)'};color:${val===cur?'#fff':'var(--muted)'};border:1px solid ${val===cur?'var(--accent)':'var(--border)'};border-radius:8px;padding:0.5rem 0.3rem;font-size:0.82rem;cursor:pointer;white-space:nowrap">${label}</button>`;
888
- const box = document.createElement('div');
889
- box.style.cssText = 'background:var(--surface);border-radius:18px 18px 0 0;width:100%;max-width:640px;padding:0.5rem 1.1rem max(1rem,env(safe-area-inset-bottom))';
890
- box.innerHTML = `<div class="sheet-handle"></div>
891
- <div style="font-size:0.95rem;font-weight:600;margin-bottom:0.6rem">外观</div>
892
- <div style="font-size:0.8rem;color:var(--text2);margin-bottom:0.35rem">主题</div>
893
- <div style="display:flex;gap:0.35rem;flex-wrap:wrap">${seg('theme','system',t,'跟随系统')}${seg('theme','dark',t,'深色')}${seg('theme','light',t,'浅色')}${seg('theme','black',t,'纯黑')}</div>
894
- <div style="font-size:0.8rem;color:var(--text2);margin:0.7rem 0 0.35rem">字号</div>
895
- <div style="display:flex;gap:0.35rem">${seg('font','small',f,'小')}${seg('font','medium',f,'中')}${seg('font','large',f,'大')}</div>
896
- <button id="ap-close" style="margin-top:0.9rem;width:100%;background:var(--bg);border:1px solid var(--border);color:var(--muted);font-size:0.85rem;padding:0.6rem;border-radius:10px;cursor:pointer">完成</button>`;
897
- overlay.appendChild(box);
898
- const close = () => overlay.remove();
899
- overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
900
- box.querySelector('#ap-close').onclick = close;
901
- box.querySelectorAll('button[data-g]').forEach(b => b.onclick = () => {
902
- localStorage.setItem(b.dataset.g === 'theme' ? 'myhi_theme' : 'myhi_font', b.dataset.v);
903
- applyAppearance();
904
- close(); openAppearance();
905
- });
906
- document.body.appendChild(overlay);
990
+ const seg = (key, opts, cur) => opts.map(([v, l]) => `<button data-k="${key}" data-v="${v}" class="${v === cur ? 'on' : ''}">${l}</button>`).join('');
991
+ document.getElementById('set-theme').innerHTML = seg('myhi_theme', THEMES, t);
992
+ document.getElementById('set-font').innerHTML = seg('myhi_font', FONTS, f);
993
+ }
994
+ document.getElementById('set-sheet').addEventListener('click', (e) => {
995
+ const b = e.target.closest('button[data-k]');
996
+ if (!b) return;
997
+ localStorage.setItem(b.dataset.k, b.dataset.v);
998
+ applyAppearance(); renderAppearance();
999
+ });
1000
+ function setStatus(id, text, color) {
1001
+ const el = document.getElementById(id);
1002
+ el.style.color = color ? `var(--${color})` : 'var(--muted)';
1003
+ el.textContent = text;
1004
+ }
1005
+ async function readJson(r) {
1006
+ // 旧 server 不认新接口时回的是 404 HTML,别让 JSON.parse 报一串天书
1007
+ const txt = await r.text();
1008
+ try { return JSON.parse(txt); } catch { throw new Error(r.status === 404 ? '服务端版本较旧,请重启 myhi 后再试' : `HTTP ${r.status}`); }
1009
+ }
1010
+
1011
+ window.openSettings = function() {
1012
+ renderAppearance();
1013
+ ['set-machine-status', 'atroot-status', 'set-asr-status', 'set-restart-status'].forEach(id => setStatus(id, ''));
1014
+ closeAtPicker();
1015
+ document.getElementById('set-backdrop').classList.add('open');
1016
+ document.getElementById('set-sheet').classList.add('open');
1017
+ fetch('/api/machine-name').then(readJson).then(d => {
1018
+ const inp = document.getElementById('set-machine-name');
1019
+ inp.placeholder = d.hostname || '';
1020
+ inp.value = d.name === d.hostname ? '' : (d.name || '');
1021
+ inp.disabled = !!d.fromEnv;
1022
+ if (d.fromEnv) setStatus('set-machine-status', '已由环境变量 MYHI_NAME 指定,此处不可改');
1023
+ }).catch(() => {});
1024
+ fetch('/api/server-config').then(readJson).then(d => {
1025
+ document.getElementById('set-asr-url').value = d.asrUrl || '';
1026
+ }).catch(() => {});
1027
+ loadAtRoots();
907
1028
  };
1029
+ window.closeSettings = function() {
1030
+ document.getElementById('set-backdrop').classList.remove('open');
1031
+ document.getElementById('set-sheet').classList.remove('open');
1032
+ };
1033
+ document.addEventListener('keydown', (e) => {
1034
+ if (e.key === 'Escape' && document.getElementById('set-sheet').classList.contains('open')) closeSettings();
1035
+ });
1036
+
1037
+ window.saveMachineName = async function() {
1038
+ try {
1039
+ const r = await fetch('/api/machine-name', {
1040
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1041
+ body: JSON.stringify({ name: document.getElementById('set-machine-name').value.trim() }),
1042
+ });
1043
+ const d = await readJson(r);
1044
+ if (!d.ok) throw new Error(d.error || '未知错误');
1045
+ window.myhiSetMachineName?.(d.name);
1046
+ setStatus('set-machine-status', `已保存:${d.name}(其他已打开的页面刷新后生效)`, 'green');
1047
+ } catch (e) { setStatus('set-machine-status', '保存失败:' + e.message, 'red'); }
1048
+ };
1049
+
1050
+ window.saveAsrUrl = async function() {
1051
+ const url = document.getElementById('set-asr-url').value.trim();
1052
+ setStatus('set-asr-status', '保存中…');
1053
+ try {
1054
+ const r = await fetch('/api/server-config', {
1055
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ asrUrl: url }),
1056
+ });
1057
+ const d = await readJson(r);
1058
+ if (!d.ok) throw new Error(d.error || '未知错误');
1059
+ setStatus('set-asr-status', url ? '已保存,重启后生效(如已设置 ASR_URL 环境变量则以环境变量为准)' : '已清空', 'green');
1060
+ } catch (e) { setStatus('set-asr-status', '保存失败:' + e.message, 'red'); }
1061
+ };
1062
+
1063
+ window.restartMyhi = async function() {
1064
+ if (!confirm('确认重启 myhi 服务?\n重启期间连接会短暂中断,页面将自动重连。')) return;
1065
+ try { await fetch('/api/restart', { method: 'POST' }); } catch {}
1066
+ setStatus('set-restart-status', '服务重启中,请稍候…');
1067
+ };
1068
+
1069
+ // @ 引用范围(项目外目录):@ 补全可引用这些目录
1070
+ let _atRoots = [];
1071
+ function renderAtRoots() {
1072
+ document.getElementById('atroots-list').innerHTML = _atRoots.length
1073
+ ? _atRoots.map((r, i) => `<div class="atr-row"><span title="${esc(r)}">📁 ${esc(r)}</span><button class="dr-btn" onclick="delAtRoot(${i})">删</button></div>`).join('')
1074
+ : '<div class="set-hint" style="margin:0">(未配置——@ 只搜任务目录)</div>';
1075
+ }
1076
+ function loadAtRoots() {
1077
+ fetch('/api/atroots').then(readJson).then(d => { _atRoots = d.roots || []; renderAtRoots(); }).catch(() => {});
1078
+ }
1079
+ async function saveAtRoots(roots) {
1080
+ try {
1081
+ const r = await fetch('/api/atroots', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ roots }) });
1082
+ const d = await readJson(r);
1083
+ if (!d.ok) throw new Error(d.error || '未知错误');
1084
+ _atRoots = d.roots; renderAtRoots(); setStatus('atroot-status', '已保存', 'green');
1085
+ return true;
1086
+ } catch (e) { setStatus('atroot-status', '保存失败:' + e.message, 'red'); return false; }
1087
+ }
1088
+ window.addAtRoot = function() {
1089
+ const inp = document.getElementById('atroot-new');
1090
+ const v = inp.value.trim();
1091
+ if (v) saveAtRoots([..._atRoots, v]).then(ok => { if (ok) inp.value = ''; });
1092
+ };
1093
+ window.delAtRoot = function(i) { saveAtRoots(_atRoots.filter((_, j) => j !== i)); };
1094
+
1095
+ // 文件夹点选(复用 /api/dirs 目录浏览):普通用户不会敲绝对路径,点进去选中即可
1096
+ let _atPick = { current: '', parent: null };
1097
+ window.openAtPicker = function() {
1098
+ document.getElementById('atpick').style.display = '';
1099
+ document.getElementById('atpick-open').style.display = 'none';
1100
+ atPickGo('');
1101
+ };
1102
+ window.closeAtPicker = function() {
1103
+ document.getElementById('atpick').style.display = 'none';
1104
+ document.getElementById('atpick-open').style.display = '';
1105
+ };
1106
+ window.atPickGo = async function(path) {
1107
+ const list = document.getElementById('atpick-list');
1108
+ list.innerHTML = '<div class="set-hint" style="padding:0.6rem;margin:0">读取中…</div>';
1109
+ try {
1110
+ const r = await fetch('/api/dirs' + (path ? '?path=' + encodeURIComponent(path) : ''));
1111
+ const d = await readJson(r);
1112
+ if (!r.ok) throw new Error(d.error || r.status);
1113
+ _atPick = d;
1114
+ const cur = document.getElementById('atpick-cur');
1115
+ cur.textContent = '\u200e' + d.current; // rtl 截断时保留路径尾部;LRM 防标点跑位
1116
+ cur.title = d.current;
1117
+ const up = document.getElementById('atpick-up');
1118
+ up.disabled = !d.parent; up.style.opacity = d.parent ? '' : '0.4';
1119
+ const dv = document.getElementById('atpick-drives');
1120
+ dv.style.display = (d.drives || []).length ? 'flex' : 'none';
1121
+ dv.innerHTML = '';
1122
+ (d.drives || []).forEach(dr => {
1123
+ const b = document.createElement('button');
1124
+ b.className = 'dr-btn';
1125
+ b.textContent = '💽 ' + dr.replace(/\\$/, '');
1126
+ if (d.current.toUpperCase().startsWith(dr.toUpperCase())) b.style.cssText = 'background:var(--accent);color:#fff;border-color:var(--accent)';
1127
+ b.onclick = () => atPickGo(dr);
1128
+ dv.appendChild(b);
1129
+ });
1130
+ list.innerHTML = d.dirs.length ? '' : '<div class="set-hint" style="padding:0.6rem;margin:0">(没有子文件夹)</div>';
1131
+ d.dirs.forEach(x => {
1132
+ const row = document.createElement('div');
1133
+ row.className = 'atpick-row';
1134
+ row.textContent = '📁 ' + x.name;
1135
+ row.onclick = () => atPickGo(x.path);
1136
+ list.appendChild(row);
1137
+ });
1138
+ } catch (e) {
1139
+ list.innerHTML = '<div style="padding:0.6rem;color:var(--red);font-size:0.75rem">读取失败:' + esc(String(e.message || e)) +
1140
+ (String(e.message).includes('权限') ? '(仅管理员可浏览目录,可展开下方「手动输入路径」)' : '') + '</div>';
1141
+ }
1142
+ };
1143
+ window.atPickUp = function() { if (_atPick.parent) atPickGo(_atPick.parent); };
1144
+ window.addAtPicked = async function() {
1145
+ const p = _atPick.current;
1146
+ if (!p) return;
1147
+ if (_atRoots.includes(p)) { setStatus('atroot-status', '这个文件夹已经在列表里了'); return; }
1148
+ if (await saveAtRoots([..._atRoots, p])) closeAtPicker();
1149
+ };
1150
+ // 会话页「更多 → 全局设置」跳过来:/?settings=1 直接打开
1151
+ if (new URLSearchParams(location.search).get('settings')) {
1152
+ history.replaceState(null, '', location.pathname);
1153
+ setTimeout(openSettings, 0);
1154
+ }
908
1155
 
909
1156
  // HTML 转义,防止 XSS
910
1157
  function esc(s) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendongfly/myhi",
3
- "version": "1.3.180",
3
+ "version": "1.3.182",
4
4
  "description": "Web-based terminal sharing with chat UI — control your terminal from phone via LAN/Tailscale",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",