@wendongfly/myhi 1.3.181 → 1.3.184

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
@@ -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,40 @@
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
+ /* 顶栏放不下一行时逐级收紧(fitHeader 按实际宽度判断,大字号/窄窗口都适用):先藏用户名,再只留图标 */
103
+ #header.hd-fit1 #user-name { display: none; }
104
+ #header.hd-fit2 .hd-btn .hd-txt { display: none; }
105
+ #header.hd-fit2 .hd-actions { gap: 0.4rem; }
106
+ #header.hd-fit2 .hd-btn { padding: 0.32rem 0.48rem; }
107
+
108
+ /* ── 设置 sheet(全局设置收口:外观 / 本机名称 / @ 引用范围 / 语音 / 账户 / 服务)── */
109
+ #set-backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 30; }
110
+ #set-sheet { display: none; position: fixed; left: 0; right: 0; bottom: 0; z-index: 31; max-width: 640px; margin: 0 auto;
111
+ 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; }
112
+ #set-sheet.open, #set-backdrop.open { display: block; }
113
+ .set-sec { padding: 0.75rem 0; border-top: 1px solid var(--border); }
114
+ .sheet-title + .set-sec { border-top: none; padding-top: 0; }
115
+ .set-sec h4 { font-size: 0.85rem; font-weight: 600; margin-bottom: 0.2rem; display: flex; align-items: center; gap: 0.4rem; }
116
+ .set-scope { font-size: 0.66rem; font-weight: 400; color: var(--muted); border: 1px solid var(--border2); border-radius: 4px; padding: 0 0.3rem; }
117
+ .set-hint { font-size: 0.72rem; color: var(--muted); line-height: 1.5; margin-bottom: 0.4rem; }
118
+ .set-status { font-size: 0.72rem; min-height: 1.1em; margin-top: 0.25rem; color: var(--muted); }
119
+ .set-row { display: flex; gap: 0.4rem; }
120
+ .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; }
121
+ .set-inp:focus { border-color: var(--accent); }
122
+ .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; }
123
+ .set-btn.ghost { background: var(--bg); color: var(--text2); border: 1px solid var(--border); }
124
+ .set-btn.danger { background: transparent; color: var(--red); border: 1px solid var(--red); }
125
+ .set-seg { display: flex; gap: 0.35rem; flex-wrap: wrap; }
126
+ .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; }
127
+ .set-seg button.on { background: var(--accent); border-color: var(--accent); color: #fff; }
128
+ .set-sub { font-size: 0.75rem; color: var(--text2); margin: 0.5rem 0 0.3rem; }
129
+ .atr-row { display: flex; align-items: center; gap: 0.4rem; padding: 0.2rem 0; font-size: 0.78rem; }
130
+ .atr-row span { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
131
+ .atpick-row { padding: 0.42rem 0.7rem; cursor: pointer; color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 0.8rem; }
132
+ .atpick-row:hover { background: var(--bg); color: var(--text); }
133
+ .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
134
  /* 手机(≤540px):藏用户名、文字标签只留图标,一行放下且不憋屈 */
100
135
  @media (max-width: 540px) {
101
136
  #user-name { display: none; }
@@ -522,20 +557,18 @@
522
557
  <div class="logo">my<span>hi</span></div>
523
558
  <div class="hd-actions">
524
559
  <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>
560
+ <button id="grid-btn" class="hd-btn" onclick="location.href='/grid'" title="分屏工作台(多任务并排)">▦<span class="hd-txt"> 分屏</span></button>
528
561
  <button id="doctor-btn" class="hd-btn" onclick="openDoctor()" title="会话体检">🩺<span class="hd-txt"> 体检</span></button>
529
562
  <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>
563
+ <button id="help-btn" class="hd-btn" onclick="openHelpSheet()" title="使用说明 / 快速上手">❓<span class="hd-txt"> 帮助</span></button>
564
+ <button id="settings-btn" class="hd-btn" onclick="openSettings()" title="设置(外观 / 本机名称 / @ 引用范围 / 语音 / 账户)">⚙<span class="hd-txt"> 设置</span></button>
532
565
  <div id="conn-dot"></div>
533
566
  </div>
534
567
  </div>
535
568
 
536
569
  <!-- 升级管理已移至 /admin -->
537
570
 
538
- <div id="view-controls" style="display:flex;flex-direction:column;gap:0.5rem;margin-bottom:0.75rem">
571
+ <div id="view-controls" style="display:flex;flex-direction:column;gap:0.5rem;padding:0.75rem 0.75rem 0">
539
572
  <!-- 会话搜索:任务多了光看标题认不出,连首条消息/目录/标签一起搜 -->
540
573
  <div style="display:flex;align-items:center;gap:0.4rem">
541
574
  <input id="sess-search" type="search" placeholder="搜任务:标题 / 首条需求 / 目录 / 标签"
@@ -625,6 +658,77 @@
625
658
  <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
659
  </div>
627
660
 
661
+ <!-- 设置 sheet:首页是全局设置的唯一入口(会话页 ⚙ 只管本设备的聊天显示/快捷栏) -->
662
+ <div id="set-backdrop" onclick="closeSettings()"></div>
663
+ <div id="set-sheet">
664
+ <div class="sheet-handle"></div>
665
+ <div class="sheet-title">⚙ 设置</div>
666
+ <div class="set-sec">
667
+ <h4>外观 <span class="set-scope">本设备</span></h4>
668
+ <div class="set-sub">主题</div>
669
+ <div class="set-seg" id="set-theme"></div>
670
+ <div class="set-sub">字号</div>
671
+ <div class="set-seg" id="set-font"></div>
672
+ </div>
673
+ <div class="set-sec">
674
+ <h4>本机名称 <span class="set-scope">全局</span></h4>
675
+ <div class="set-hint">多台电脑都跑 myhi 时用来区分:浏览器标题以它开头,标签图标按名字自动配色。留空 = 用电脑主机名。</div>
676
+ <div class="set-row">
677
+ <input id="set-machine-name" class="set-inp" maxlength="32" autocomplete="off">
678
+ <button class="set-btn" onclick="saveMachineName()">保存</button>
679
+ </div>
680
+ <div id="set-machine-status" class="set-status"></div>
681
+ </div>
682
+ <div class="set-sec">
683
+ <h4>@ 引用范围 <span class="set-scope">全局</span></h4>
684
+ <div class="set-hint">除了任务目录,再把常用的资料文件夹(如公共资料库)加进来,打 @ 时也能搜到里面的文件。</div>
685
+ <div id="atroots-list"></div>
686
+ <button id="atpick-open" class="set-btn" onclick="openAtPicker()" style="width:100%;margin-top:0.4rem">📁 选择文件夹添加…</button>
687
+ <div id="atpick" style="display:none;margin-top:0.4rem;border:1px solid var(--border);border-radius:10px;overflow:hidden">
688
+ <div id="atpick-drives" style="display:none;gap:0.3rem;padding:0.4rem 0.5rem 0;flex-wrap:wrap"></div>
689
+ <div style="display:flex;align-items:center;gap:0.35rem;padding:0.4rem 0.5rem;border-bottom:1px solid var(--border)">
690
+ <button id="atpick-up" class="dr-btn" onclick="atPickUp()" title="上一级">⬆ 上一级</button>
691
+ <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>
692
+ </div>
693
+ <div id="atpick-list" style="max-height:200px;overflow-y:auto;scrollbar-width:thin"></div>
694
+ <div style="display:flex;gap:0.4rem;padding:0.45rem 0.5rem;border-top:1px solid var(--border)">
695
+ <button class="set-btn ghost" onclick="closeAtPicker()" style="flex:1">取消</button>
696
+ <button class="set-btn" onclick="addAtPicked()" style="flex:2">✓ 添加当前文件夹</button>
697
+ </div>
698
+ </div>
699
+ <details style="margin-top:0.35rem;font-size:0.72rem;color:var(--muted)">
700
+ <summary style="cursor:pointer">手动输入路径</summary>
701
+ <div class="set-row" style="margin-top:0.35rem">
702
+ <input id="atroot-new" class="set-inp" placeholder="例如 D:\资料库" autocomplete="off">
703
+ <button class="set-btn" onclick="addAtRoot()">添加</button>
704
+ </div>
705
+ </details>
706
+ <div id="atroot-status" class="set-status"></div>
707
+ </div>
708
+ <div class="set-sec">
709
+ <h4>语音识别 (ASR) <span class="set-scope">全局</span></h4>
710
+ <div class="set-hint">SenseVoice 或兼容服务的地址,留空表示禁用语音。</div>
711
+ <div class="set-row">
712
+ <input id="set-asr-url" class="set-inp" placeholder="http://192.168.x.x:8765" autocomplete="off">
713
+ <button class="set-btn" onclick="saveAsrUrl()">保存</button>
714
+ </div>
715
+ <div id="set-asr-status" class="set-status"></div>
716
+ </div>
717
+ <div class="set-sec" id="set-account" style="display:none">
718
+ <h4>账户 <span id="set-account-name" class="set-scope"></span></h4>
719
+ <div class="set-row" style="margin-top:0.4rem">
720
+ <button id="passwd-btn" class="set-btn ghost" onclick="closeSettings();openPasswdSheet()" style="flex:1;display:none">🔑 修改密码</button>
721
+ <button id="logout-btn" class="set-btn danger" onclick="doLogout()" style="flex:1;display:none">退出登录</button>
722
+ </div>
723
+ </div>
724
+ <div class="set-sec">
725
+ <h4>服务</h4>
726
+ <div class="set-hint">聊天显示、回车键、快捷栏按钮在会话页的 ⚙ 设置里调(只影响本设备)。</div>
727
+ <button class="set-btn danger" onclick="restartMyhi()" style="width:100%">重启 myhi</button>
728
+ <div id="set-restart-status" class="set-status"></div>
729
+ </div>
730
+ </div>
731
+
628
732
  <!-- 修改密码 sheet(成员用户自助改密;管理员改密码在 /admin) -->
629
733
  <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
734
  <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 +855,14 @@
751
855
  fetch('/api/me').then(r => r.json()).then(data => {
752
856
  if ((data.exclusive || data.hasUsers) && data.name) {
753
857
  document.getElementById('user-name').textContent = data.name;
858
+ document.getElementById('set-account-name').textContent = data.name;
859
+ fitHeader();
860
+ document.getElementById('set-account').style.display = '';
754
861
  document.getElementById('logout-btn').style.display = '';
755
862
  }
756
863
  // 成员用户(users.json 登录)才显示自助改密入口;管理员改密码在 /admin
757
864
  if (data.hasUsers && data.role === 'operator') {
865
+ document.getElementById('set-account').style.display = '';
758
866
  document.getElementById('passwd-btn').style.display = '';
759
867
  }
760
868
  if (data.dir) {
@@ -878,33 +986,191 @@
878
986
  r.setAttribute('data-font', f);
879
987
  const meta = document.querySelector('meta[name="theme-color"]');
880
988
  if (meta) meta.setAttribute('content', THEME_META[t] || '#0d1117');
989
+ fitHeader();
990
+ }
991
+ // 顶栏按钮组折行了就逐级收紧:字号「大」时按钮跟着 rem 变宽,固定断点兜不住
992
+ function fitHeader() {
993
+ const h = document.getElementById('header');
994
+ const acts = h.querySelector('.hd-actions');
995
+ const wraps = () => acts.offsetHeight > document.getElementById('settings-btn').offsetHeight * 1.5;
996
+ h.classList.remove('hd-fit1', 'hd-fit2');
997
+ if (wraps()) h.classList.add('hd-fit1');
998
+ if (wraps()) h.classList.add('hd-fit2');
881
999
  }
882
- window.openAppearance = function() {
1000
+ window.addEventListener('resize', fitHeader);
1001
+ document.fonts?.ready.then(fitHeader);
1002
+ fitHeader();
1003
+ // ── 设置 sheet ─────────────────────────────────────────
1004
+ const THEMES = [['system','跟随系统'],['dark','深色'],['light','浅色'],['black','纯黑']];
1005
+ const FONTS = [['small','小'],['medium','中'],['large','大']];
1006
+ function renderAppearance() {
883
1007
  const t = localStorage.getItem('myhi_theme') || 'dark';
884
1008
  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);
1009
+ const seg = (key, opts, cur) => opts.map(([v, l]) => `<button data-k="${key}" data-v="${v}" class="${v === cur ? 'on' : ''}">${l}</button>`).join('');
1010
+ document.getElementById('set-theme').innerHTML = seg('myhi_theme', THEMES, t);
1011
+ document.getElementById('set-font').innerHTML = seg('myhi_font', FONTS, f);
1012
+ }
1013
+ document.getElementById('set-sheet').addEventListener('click', (e) => {
1014
+ const b = e.target.closest('button[data-k]');
1015
+ if (!b) return;
1016
+ localStorage.setItem(b.dataset.k, b.dataset.v);
1017
+ applyAppearance(); renderAppearance();
1018
+ });
1019
+ function setStatus(id, text, color) {
1020
+ const el = document.getElementById(id);
1021
+ el.style.color = color ? `var(--${color})` : 'var(--muted)';
1022
+ el.textContent = text;
1023
+ }
1024
+ async function readJson(r) {
1025
+ // 旧 server 不认新接口时回的是 404 HTML,别让 JSON.parse 报一串天书
1026
+ const txt = await r.text();
1027
+ try { return JSON.parse(txt); } catch { throw new Error(r.status === 404 ? '服务端版本较旧,请重启 myhi 后再试' : `HTTP ${r.status}`); }
1028
+ }
1029
+
1030
+ window.openSettings = function() {
1031
+ renderAppearance();
1032
+ ['set-machine-status', 'atroot-status', 'set-asr-status', 'set-restart-status'].forEach(id => setStatus(id, ''));
1033
+ closeAtPicker();
1034
+ document.getElementById('set-backdrop').classList.add('open');
1035
+ document.getElementById('set-sheet').classList.add('open');
1036
+ fetch('/api/machine-name').then(readJson).then(d => {
1037
+ const inp = document.getElementById('set-machine-name');
1038
+ inp.placeholder = d.hostname || '';
1039
+ inp.value = d.name === d.hostname ? '' : (d.name || '');
1040
+ inp.disabled = !!d.fromEnv;
1041
+ if (d.fromEnv) setStatus('set-machine-status', '已由环境变量 MYHI_NAME 指定,此处不可改');
1042
+ }).catch(() => {});
1043
+ fetch('/api/server-config').then(readJson).then(d => {
1044
+ document.getElementById('set-asr-url').value = d.asrUrl || '';
1045
+ }).catch(() => {});
1046
+ loadAtRoots();
907
1047
  };
1048
+ window.closeSettings = function() {
1049
+ document.getElementById('set-backdrop').classList.remove('open');
1050
+ document.getElementById('set-sheet').classList.remove('open');
1051
+ };
1052
+ document.addEventListener('keydown', (e) => {
1053
+ if (e.key === 'Escape' && document.getElementById('set-sheet').classList.contains('open')) closeSettings();
1054
+ });
1055
+
1056
+ window.saveMachineName = async function() {
1057
+ try {
1058
+ const r = await fetch('/api/machine-name', {
1059
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1060
+ body: JSON.stringify({ name: document.getElementById('set-machine-name').value.trim() }),
1061
+ });
1062
+ const d = await readJson(r);
1063
+ if (!d.ok) throw new Error(d.error || '未知错误');
1064
+ window.myhiSetMachineName?.(d.name);
1065
+ setStatus('set-machine-status', `已保存:${d.name}(其他已打开的页面刷新后生效)`, 'green');
1066
+ } catch (e) { setStatus('set-machine-status', '保存失败:' + e.message, 'red'); }
1067
+ };
1068
+
1069
+ window.saveAsrUrl = async function() {
1070
+ const url = document.getElementById('set-asr-url').value.trim();
1071
+ setStatus('set-asr-status', '保存中…');
1072
+ try {
1073
+ const r = await fetch('/api/server-config', {
1074
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ asrUrl: url }),
1075
+ });
1076
+ const d = await readJson(r);
1077
+ if (!d.ok) throw new Error(d.error || '未知错误');
1078
+ setStatus('set-asr-status', url ? '已保存,重启后生效(如已设置 ASR_URL 环境变量则以环境变量为准)' : '已清空', 'green');
1079
+ } catch (e) { setStatus('set-asr-status', '保存失败:' + e.message, 'red'); }
1080
+ };
1081
+
1082
+ window.restartMyhi = async function() {
1083
+ if (!confirm('确认重启 myhi 服务?\n重启期间连接会短暂中断,页面将自动重连。')) return;
1084
+ try { await fetch('/api/restart', { method: 'POST' }); } catch {}
1085
+ setStatus('set-restart-status', '服务重启中,请稍候…');
1086
+ };
1087
+
1088
+ // @ 引用范围(项目外目录):@ 补全可引用这些目录
1089
+ let _atRoots = [];
1090
+ function renderAtRoots() {
1091
+ document.getElementById('atroots-list').innerHTML = _atRoots.length
1092
+ ? _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('')
1093
+ : '<div class="set-hint" style="margin:0">(未配置——@ 只搜任务目录)</div>';
1094
+ }
1095
+ function loadAtRoots() {
1096
+ fetch('/api/atroots').then(readJson).then(d => { _atRoots = d.roots || []; renderAtRoots(); }).catch(() => {});
1097
+ }
1098
+ async function saveAtRoots(roots) {
1099
+ try {
1100
+ const r = await fetch('/api/atroots', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ roots }) });
1101
+ const d = await readJson(r);
1102
+ if (!d.ok) throw new Error(d.error || '未知错误');
1103
+ _atRoots = d.roots; renderAtRoots(); setStatus('atroot-status', '已保存', 'green');
1104
+ return true;
1105
+ } catch (e) { setStatus('atroot-status', '保存失败:' + e.message, 'red'); return false; }
1106
+ }
1107
+ window.addAtRoot = function() {
1108
+ const inp = document.getElementById('atroot-new');
1109
+ const v = inp.value.trim();
1110
+ if (v) saveAtRoots([..._atRoots, v]).then(ok => { if (ok) inp.value = ''; });
1111
+ };
1112
+ window.delAtRoot = function(i) { saveAtRoots(_atRoots.filter((_, j) => j !== i)); };
1113
+
1114
+ // 文件夹点选(复用 /api/dirs 目录浏览):普通用户不会敲绝对路径,点进去选中即可
1115
+ let _atPick = { current: '', parent: null };
1116
+ window.openAtPicker = function() {
1117
+ document.getElementById('atpick').style.display = '';
1118
+ document.getElementById('atpick-open').style.display = 'none';
1119
+ atPickGo('');
1120
+ };
1121
+ window.closeAtPicker = function() {
1122
+ document.getElementById('atpick').style.display = 'none';
1123
+ document.getElementById('atpick-open').style.display = '';
1124
+ };
1125
+ window.atPickGo = async function(path) {
1126
+ const list = document.getElementById('atpick-list');
1127
+ list.innerHTML = '<div class="set-hint" style="padding:0.6rem;margin:0">读取中…</div>';
1128
+ try {
1129
+ const r = await fetch('/api/dirs' + (path ? '?path=' + encodeURIComponent(path) : ''));
1130
+ const d = await readJson(r);
1131
+ if (!r.ok) throw new Error(d.error || r.status);
1132
+ _atPick = d;
1133
+ const cur = document.getElementById('atpick-cur');
1134
+ cur.textContent = '\u200e' + d.current; // rtl 截断时保留路径尾部;LRM 防标点跑位
1135
+ cur.title = d.current;
1136
+ const up = document.getElementById('atpick-up');
1137
+ up.disabled = !d.parent; up.style.opacity = d.parent ? '' : '0.4';
1138
+ const dv = document.getElementById('atpick-drives');
1139
+ dv.style.display = (d.drives || []).length ? 'flex' : 'none';
1140
+ dv.innerHTML = '';
1141
+ (d.drives || []).forEach(dr => {
1142
+ const b = document.createElement('button');
1143
+ b.className = 'dr-btn';
1144
+ b.textContent = '💽 ' + dr.replace(/\\$/, '');
1145
+ if (d.current.toUpperCase().startsWith(dr.toUpperCase())) b.style.cssText = 'background:var(--accent);color:#fff;border-color:var(--accent)';
1146
+ b.onclick = () => atPickGo(dr);
1147
+ dv.appendChild(b);
1148
+ });
1149
+ list.innerHTML = d.dirs.length ? '' : '<div class="set-hint" style="padding:0.6rem;margin:0">(没有子文件夹)</div>';
1150
+ d.dirs.forEach(x => {
1151
+ const row = document.createElement('div');
1152
+ row.className = 'atpick-row';
1153
+ row.textContent = '📁 ' + x.name;
1154
+ row.onclick = () => atPickGo(x.path);
1155
+ list.appendChild(row);
1156
+ });
1157
+ } catch (e) {
1158
+ list.innerHTML = '<div style="padding:0.6rem;color:var(--red);font-size:0.75rem">读取失败:' + esc(String(e.message || e)) +
1159
+ (String(e.message).includes('权限') ? '(仅管理员可浏览目录,可展开下方「手动输入路径」)' : '') + '</div>';
1160
+ }
1161
+ };
1162
+ window.atPickUp = function() { if (_atPick.parent) atPickGo(_atPick.parent); };
1163
+ window.addAtPicked = async function() {
1164
+ const p = _atPick.current;
1165
+ if (!p) return;
1166
+ if (_atRoots.includes(p)) { setStatus('atroot-status', '这个文件夹已经在列表里了'); return; }
1167
+ if (await saveAtRoots([..._atRoots, p])) closeAtPicker();
1168
+ };
1169
+ // 会话页「更多 → 全局设置」跳过来:/?settings=1 直接打开
1170
+ if (new URLSearchParams(location.search).get('settings')) {
1171
+ history.replaceState(null, '', location.pathname);
1172
+ setTimeout(openSettings, 0);
1173
+ }
908
1174
 
909
1175
  // HTML 转义,防止 XSS
910
1176
  function esc(s) {