@wendongfly/myhi 1.3.188 → 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) {
@@ -187,6 +200,13 @@
187
200
  }
188
201
  .empty-state svg { opacity: 0.3; }
189
202
  .empty-state p { font-size: 0.95rem; }
203
+ .empty-new { background: var(--accent); color: #fff; border: none; border-radius: 10px; padding: 0.65rem 1.4rem; font-size: 0.92rem; font-weight: 600; cursor: pointer; box-shadow: 0 4px 16px rgba(124,58,237,0.35); }
204
+ .empty-new:hover { background: var(--accent2); }
205
+ .empty-state small { font-size: 0.72rem; opacity: 0.8; }
206
+ /* 搜索框旁的「+ 新建任务」:大屏上右下角 FAB 离内容太远容易看不到;手机上有 FAB 就够了 */
207
+ #new-btn { flex-shrink: 0; white-space: nowrap; background: var(--accent); color: #fff; border: none; border-radius: 8px; font-size: 0.8rem; font-weight: 600; padding: 0.42rem 0.85rem; cursor: pointer; }
208
+ #new-btn:hover { background: var(--accent2); }
209
+ @media (max-width: 540px) { #new-btn { display: none; } }
190
210
 
191
211
  /* ── Session card ── */
192
212
  .card {
@@ -283,6 +303,14 @@
283
303
  z-index: 20;
284
304
  }
285
305
  #fab:active { background: var(--accent2); transform: scale(0.93); }
306
+ /* 首次引导关掉后:FAB 呼吸光圈 + 气泡指一下「点这里新建任务」,只出现一次 */
307
+ #fab.hint { animation: fabPulse 1.4s ease-out infinite; }
308
+ @keyframes fabPulse { 0% { box-shadow: 0 0 0 0 rgba(124,58,237,0.7), 0 4px 20px rgba(124,58,237,0.5); } 100% { box-shadow: 0 0 0 22px rgba(124,58,237,0), 0 4px 20px rgba(124,58,237,0.5); } }
309
+ #fab-tip { display: none; position: fixed; z-index: 21; right: max(5.2rem, calc(env(safe-area-inset-right) + 4.7rem)); bottom: max(1.85rem, calc(env(safe-area-inset-bottom) + 1.35rem));
310
+ background: var(--accent); color: #fff; font-size: 0.85rem; font-weight: 600; padding: 0.5rem 0.8rem; border-radius: 10px; white-space: nowrap; box-shadow: 0 4px 16px rgba(0,0,0,0.35); cursor: pointer; }
311
+ #fab-tip::after { content: ''; position: absolute; right: -6px; top: 50%; margin-top: -6px; border: 6px solid transparent; border-right: none; border-left-color: var(--accent); }
312
+ #fab-tip.show { display: block; animation: tipIn 0.3s ease; }
313
+ @keyframes tipIn { from { opacity: 0; transform: translateX(8px); } to { opacity: 1; transform: none; } }
286
314
 
287
315
  /* 随手记 FAB:灵感常常在你不在任何会话页的时候冒出来,首页也得能记 */
288
316
  #fab-note {
@@ -596,6 +624,8 @@
596
624
  </div>
597
625
 
598
626
  <!-- 升级管理已移至 /admin -->
627
+ <!-- AI 引擎(Claude Code)没装时的提示:npm 装 myhi 不会顺带装它,新机器第一次用最容易卡在这 -->
628
+ <div id="ai-banner" class="ai-banner" style="display:none"></div>
599
629
 
600
630
  <div id="view-controls" style="display:flex;flex-direction:column;gap:0.5rem;padding:0.75rem 0.75rem 0">
601
631
  <!-- 会话搜索:任务多了光看标题认不出,连首条消息/目录/标签一起搜 -->
@@ -604,6 +634,7 @@
604
634
  autocomplete="off" spellcheck="false"
605
635
  style="flex:1;background:var(--bg,var(--bg));border:1px solid var(--border,var(--border));border-radius:8px;color:var(--text,var(--text));font-size:0.8rem;padding:0.4rem 0.7rem;outline:none">
606
636
  <span id="search-count" style="font-size:0.72rem;color:var(--muted,var(--muted));white-space:nowrap"></span>
637
+ <button id="new-btn" onclick="openSheet()" title="新建任务(也可点右下角 +)">+ 新建任务</button>
607
638
  </div>
608
639
  <div id="group-seg" style="display:inline-flex;align-self:flex-start;background:var(--bg,var(--bg));border:1px solid var(--border,var(--border));border-radius:8px;overflow:hidden">
609
640
  <button class="grp-btn" data-group="time" style="background:none;border:none;color:var(--muted,var(--muted));font-size:0.78rem;padding:0.35rem 0.8rem;cursor:pointer">最近</button>
@@ -619,13 +650,16 @@
619
650
  <rect x="2" y="4" width="20" height="16" rx="2"/>
620
651
  <path d="M8 9l4 3-4 3M13 15h3"/>
621
652
  </svg>
622
- <p>还没有终端会话</p>
653
+ <p>还没有任务</p>
654
+ <button class="empty-new" onclick="openSheet()">+ 新建第一个任务</button>
655
+ <small>以后随时点右下角的 + 新建</small>
623
656
  </div>
624
657
  </div>
625
658
  </div>
626
659
 
627
660
  <!-- FAB -->
628
- <button id="fab" onclick="openSheet()">+</button>
661
+ <button id="fab" onclick="openSheet()" title="新建任务">+</button>
662
+ <div id="fab-tip" onclick="openSheet()">👉 点这里新建任务</div>
629
663
  <button id="fab-note" onclick="openNoteSheet()" title="随手记:把想法先记进某个任务的草稿箱">📥</button>
630
664
 
631
665
  <!-- 使用说明 / 快速上手(首次访问自动弹出,之后从顶栏 ❓ 打开)。桌面居中卡片,手机底部抽屉 -->
@@ -653,7 +687,7 @@
653
687
  <div class="hs-tip">📱 手机浏览器打开同一地址即可(局域网 / Tailscale),建议「添加到主屏幕」当 App 用。</div>
654
688
  <div class="hs-actions">
655
689
  <button class="hs-ghost" onclick="closeHelpSheet()">我知道了</button>
656
- <button class="hs-primary" onclick="closeHelpSheet(); openSheet();">+ 新建第一个任务</button>
690
+ <button class="hs-primary" onclick="closeHelpSheet(true); openSheet();">+ 新建第一个任务</button>
657
691
  </div>
658
692
  </div>
659
693
 
@@ -699,6 +733,7 @@
699
733
  <div class="sheet-handle"></div>
700
734
  <div class="set-head"><b>设置</b><button class="set-x" onclick="closeSettings()" title="关闭 (Esc)">✕</button></div>
701
735
  </div>
736
+ <div id="set-mode" class="set-mode"></div>
702
737
 
703
738
  <div class="set-group-t">本设备</div>
704
739
  <div class="set-card">
@@ -709,7 +744,7 @@
709
744
  <div class="set-group-t">这台电脑 · 所有设备生效</div>
710
745
  <div class="set-card">
711
746
  <div class="set-item inp">
712
- <div class="set-lbl">本机名称<small>显示在浏览器标签上</small></div>
747
+ <div class="set-lbl">本机显示名<small>首页 / 分屏的浏览器标签显示它,不改电脑主机名;留空用主机名</small></div>
713
748
  <div class="set-ctl">
714
749
  <input id="set-machine-name" class="set-inp" maxlength="32" autocomplete="off" data-save="saveMachineName">
715
750
  <button class="set-btn" onclick="saveMachineName()">保存</button>
@@ -726,12 +761,19 @@
726
761
  <div id="set-asr-status" class="set-status"></div>
727
762
  </div>
728
763
 
729
- <div class="set-group-t">服务</div>
730
- <div class="set-card">
731
- <a id="set-admin-link" class="set-link" href="/admin" style="display:none">🛠 管理后台 <small>用户 · 网关 · 密钥 · 升级</small></a>
732
- <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>
733
776
  </div>
734
- <div id="set-restart-status" class="set-status" style="padding:0.45rem 0.25rem 0;margin:0"></div>
735
777
  <div class="set-foot">聊天显示、回车键、快捷栏在会话页右上角 ⚙ 设置</div>
736
778
  </div>
737
779
 
@@ -858,6 +900,7 @@
858
900
 
859
901
  // 加载当前用户信息(独占模式显示用户名、退出按钮、锁定目录)
860
902
  let _userDir = null;
903
+ let _isAdmin = true;
861
904
  fetch('/api/me').then(r => r.json()).then(data => {
862
905
  if ((data.exclusive || data.hasUsers) && data.name) {
863
906
  document.getElementById('user-name').textContent = data.name;
@@ -866,8 +909,14 @@
866
909
  fitHeader();
867
910
  }
868
911
  // 成员用户(users.json 登录)才显示自助改密入口;管理员改密码在 /admin
869
- if (data.role === 'admin') document.getElementById('set-admin-link').style.display = '';
870
- 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();
871
920
  if (data.hasUsers && data.role === 'operator') document.getElementById('passwd-btn').style.display = '';
872
921
  if (data.dir) {
873
922
  _userDir = data.dir;
@@ -1033,6 +1082,7 @@
1033
1082
  const b = e.target.closest('button[data-k]');
1034
1083
  if (!b) return;
1035
1084
  localStorage.setItem(b.dataset.k, b.dataset.v);
1085
+ window.myhiPrefsPush?.(); // 同步到其他设备
1036
1086
  applyAppearance(); renderAppearance();
1037
1087
  });
1038
1088
  function setStatus(id, text, color) {
@@ -1048,7 +1098,7 @@
1048
1098
 
1049
1099
  window.openSettings = function() {
1050
1100
  renderAppearance();
1051
- ['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, ''));
1052
1102
  document.querySelectorAll('#set-sheet .set-btn').forEach(b => b.classList.remove('dirty'));
1053
1103
  document.getElementById('set-backdrop').classList.add('open');
1054
1104
  document.getElementById('set-sheet').classList.add('open');
@@ -1056,12 +1106,17 @@
1056
1106
  const inp = document.getElementById('set-machine-name');
1057
1107
  inp.placeholder = d.hostname || '';
1058
1108
  inp.value = d.name === d.hostname ? '' : (d.name || '');
1059
- inp.disabled = !!d.fromEnv;
1109
+ inp.disabled = !!d.fromEnv || !_isAdmin;
1060
1110
  if (d.fromEnv) setStatus('set-machine-status', '已由环境变量 MYHI_NAME 指定,此处不可改');
1111
+ else if (!_isAdmin) setStatus('set-machine-status', '仅管理员可修改');
1061
1112
  }).catch(() => {});
1062
1113
  fetch('/api/server-config').then(readJson).then(d => {
1063
- 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', '仅管理员可修改');
1064
1118
  }).catch(() => {});
1119
+ checkHomeVersion();
1065
1120
  };
1066
1121
  // 输入框改了内容 → 保存按钮亮起;回车 = 保存
1067
1122
  document.querySelectorAll('#set-sheet input[data-save]').forEach(inp => {
@@ -1106,6 +1161,94 @@
1106
1161
  } catch (e) { setStatus('set-asr-status', '保存失败:' + e.message, 'red'); }
1107
1162
  };
1108
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
+
1109
1252
  window.restartMyhi = async function() {
1110
1253
  if (!confirm('确认重启 myhi 服务?\n重启期间连接会短暂中断,页面将自动重连。')) return;
1111
1254
  try { await fetch('/api/restart', { method: 'POST' }); } catch {}
@@ -1224,7 +1367,9 @@
1224
1367
  <rect x="2" y="4" width="20" height="16" rx="2"/>
1225
1368
  <path d="M8 9l4 3-4 3M13 15h3"/>
1226
1369
  </svg>
1227
- <p>还没有终端会话</p>
1370
+ <p>还没有任务</p>
1371
+ <button class="empty-new" onclick="openSheet()">+ 新建第一个任务</button>
1372
+ <small>以后随时点右下角的 + 新建</small>
1228
1373
  </div>`;
1229
1374
 
1230
1375
  function cardHTML(s) {
@@ -1422,11 +1567,21 @@
1422
1567
  document.getElementById('help-backdrop').style.display = 'block';
1423
1568
  document.getElementById('help-sheet').style.display = 'block';
1424
1569
  };
1425
- window.closeHelpSheet = function() {
1570
+ window.closeHelpSheet = function(creating) {
1426
1571
  document.getElementById('help-backdrop').style.display = 'none';
1427
1572
  document.getElementById('help-sheet').style.display = 'none';
1573
+ const first = !localStorage.getItem('myhi_onboarded');
1428
1574
  localStorage.setItem('myhi_onboarded', '1'); // 看过一次就不再自动弹
1575
+ // 第一次看完引导点的是「我知道了 / ✕」:把用户的视线引到右下角 +(大屏上它离内容很远,容易看不到)
1576
+ if (first && !creating) showFabHint();
1429
1577
  };
1578
+ function showFabHint() {
1579
+ const fab = document.getElementById('fab'), tip = document.getElementById('fab-tip');
1580
+ fab.classList.add('hint'); tip.classList.add('show');
1581
+ const done = () => { fab.classList.remove('hint'); tip.classList.remove('show'); document.removeEventListener('pointerdown', done, true); };
1582
+ setTimeout(() => document.addEventListener('pointerdown', done, true), 300); // 点任何地方(含 + 本身)就收起
1583
+ setTimeout(done, 12000);
1584
+ }
1430
1585
  // 首次访问:自动弹快速上手,带着建第一个任务
1431
1586
  if (!localStorage.getItem('myhi_onboarded') && !new URLSearchParams(location.search).get('embed')) {
1432
1587
  setTimeout(openHelpSheet, 600);