@wendongfly/myhi 1.3.187 → 1.3.190

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
@@ -9,7 +9,7 @@
9
9
  <link rel="manifest" href="/manifest.json">
10
10
  <link rel="apple-touch-icon" href="/icon.png">
11
11
  <title>myhi 会话</title>
12
- <script src="/lib/machine.js"></script>
12
+ <script src="/lib/machine.js" data-session></script>
13
13
  <script src="/sio-client/socket.io.min.js"></script>
14
14
  <script>
15
15
  (function(){try{
@@ -202,6 +202,9 @@
202
202
  .msg-raw:hover .raw-copy, .raw-copy.done { opacity: 1; }
203
203
  .raw-copy:hover, .cp-btn:hover { color: var(--text); border-color: var(--blue); }
204
204
  @media (hover: none) { .raw-copy { opacity: 0.85; } }
205
+ /* AI 工具的原始输出默认不显示(设置→聊天→显示 打开;终端会话的命令输出不受影响) */
206
+ body.hide-toolout .msg-raw.tool-out { display: none; }
207
+ .setting-toggle.off { opacity: 0.45; pointer-events: none; }
205
208
  /* 消息里其他带框内容(代码块/表格/diff/工具详情…)同款复制按钮,见 attachCopyBtns */
206
209
  .cpw, .cp-host { position: relative; }
207
210
  .cp-btn { user-select: none; }
@@ -647,7 +650,8 @@
647
650
  <label class="setting-toggle"><span>显示思考过程</span><input type="checkbox" id="set-thinking" checked></label>
648
651
  <label class="setting-toggle"><span>工具调用默认折叠</span><input type="checkbox" id="set-collapse" checked></label>
649
652
  <label class="setting-toggle"><span>思考过程默认折叠</span><input type="checkbox" id="set-thinkcollapse" checked></label>
650
- <label class="setting-toggle"><span>工具输出只显示前 8 行(可展开)</span><input type="checkbox" id="set-rawclip" checked></label>
653
+ <label class="setting-toggle"><span>显示工具输出内容(AI 执行命令/读文件的原始结果)</span><input type="checkbox" id="set-toolout"></label>
654
+ <label class="setting-toggle" id="row-rawclip"><span> └ 工具输出只显示前 8 行(可展开)</span><input type="checkbox" id="set-rawclip" checked></label>
651
655
  </div>
652
656
  <div class="setting-group" data-stab="chat" style="display:none">
653
657
  <h4>输入</h4>
@@ -1209,7 +1213,7 @@
1209
1213
  // 快捷栏:skVisible 逐按钮勾选(唯一显隐依据,null=默认全显);skMode 仅记录最接近的预设(高亮用)
1210
1214
  const SK_LABELS = { draft:'📥 草稿', folder:'📂 文件夹', model:'模型', plan:'计划', compact:'压缩', resume:'恢复', clear:'清除', secrets:'🔑 密钥', guard:'🛡 护栏', followup:'🔄 跟进', git:'提交', memory:'记忆', skills:'技能', cost:'费用' };
1211
1215
  const SK_NORMAL_PRESET = ['draft', 'folder', 'plan', 'clear', 'guard', 'memory', 'skills'];
1212
- const DEFAULT_SETTINGS = { parseOutput: true, showThinking: true, collapseTools: true, thinkCollapsed: true, rawClip: true, enterMode: 'auto', skMode: null, skVisible: null };
1216
+ const DEFAULT_SETTINGS = { parseOutput: true, showThinking: true, collapseTools: true, thinkCollapsed: true, rawClip: true, showToolOut: false, enterMode: 'auto', skMode: null, skVisible: null };
1213
1217
  let settings = loadSettings();
1214
1218
 
1215
1219
  function loadSettings() { try { return { ...DEFAULT_SETTINGS, ...JSON.parse(localStorage.getItem('myhi_chat_settings')) }; } catch { return { ...DEFAULT_SETTINGS }; } }
@@ -1315,6 +1319,21 @@
1315
1319
  let _historyLoading = false; // 防抖锁
1316
1320
  let _userScrolledUp = false; // 用户已主动往上翻 → 关掉 trim 防被自动删
1317
1321
  const _histFingerprints = new Set(); // 已渲染历史消息指纹(翻页重叠兜底去重,agent:history 重放时清空)
1322
+ // 回放里已画过的块(工具调用 id / 工具结果 id / 文本按 message.id)。AI 正在跑时进入会话(分屏盯任务最常见):
1323
+ // 服务端从 transcript 回放时,最新几条已经写进 transcript,紧接着同样的块又作为实时事件推过来 → 画两次。
1324
+ // 实时事件按这些 id 查重,回放过的就跳过;agent:history 重放时清空。
1325
+ const _replayedBlocks = new Set();
1326
+ function blockKey(block, msgId) {
1327
+ if (block.type === 'tool_use' && block.id) return 'tu:' + block.id;
1328
+ if (block.type === 'tool_result' && block.tool_use_id) return 'tr:' + block.tool_use_id;
1329
+ if (block.type === 'text' && block.text && msgId) return 'tx:' + msgId + ':' + block.text.slice(0, 80);
1330
+ return null;
1331
+ }
1332
+ // 实时块是否在回放里画过(画过返回 true,调用方跳过)
1333
+ function seenInReplay(block, msgId) {
1334
+ const k = blockKey(block, msgId);
1335
+ return !!k && _replayedBlocks.has(k);
1336
+ }
1318
1337
 
1319
1338
  // 命令历史
1320
1339
  const cmdHistory = [];
@@ -2080,6 +2099,9 @@
2080
2099
  }
2081
2100
 
2082
2101
  function renderHistoryMessage(msg, parent) {
2102
+ if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
2103
+ for (const b of msg.message.content) { const k = blockKey(b, msg.message.id); if (k) _replayedBlocks.add(k); }
2104
+ } else if (msg.type === 'tool_result' && msg.id) _replayedBlocks.add('tr:' + msg.id);
2083
2105
  if (subaRoute(msg)) return; // 子代理内部历史消息归入对应面板(_history 兜底路径才有)
2084
2106
  const fp = histFingerprint(msg);
2085
2107
  if (fp) {
@@ -2094,7 +2116,7 @@
2094
2116
  if (!text) return;
2095
2117
  // 与 live 一致:工具输出 = 工具卡后面的独立 raw 代码块(addRawOutput 同款)
2096
2118
  const el = document.createElement('div');
2097
- el.className = 'msg msg-raw';
2119
+ el.className = 'msg msg-raw tool-out';
2098
2120
  const block = document.createElement('div');
2099
2121
  block.className = 'code-block';
2100
2122
  block.innerHTML = renderAnsiHtml(text);
@@ -2268,13 +2290,14 @@
2268
2290
  }
2269
2291
  }
2270
2292
 
2271
- function addRawOutput(raw) {
2293
+ // tool=true:AI 工具的输出(受「显示工具输出内容」开关控制);否则是终端会话的命令输出,始终显示
2294
+ function addRawOutput(raw, tool = false) {
2272
2295
  endStream(); endToolGroup();
2273
2296
  removeThinking();
2274
2297
  if (!stripAnsi(raw).trim()) return;
2275
- // 合并到上一个 raw 输出块(避免 DOM 碎片化)
2298
+ // 合并到上一个同类 raw 输出块(避免 DOM 碎片化)
2276
2299
  const last = chatArea.lastElementChild;
2277
- if (last?.classList.contains('msg-raw')) {
2300
+ if (last?.classList.contains('msg-raw') && last.classList.contains('tool-out') === tool) {
2278
2301
  const block = last.querySelector('.code-block');
2279
2302
  if (block) {
2280
2303
  block.innerHTML += renderAnsiHtml(raw);
@@ -2287,7 +2310,7 @@
2287
2310
  }
2288
2311
  }
2289
2312
  const msg = document.createElement('div');
2290
- msg.className = 'msg msg-raw';
2313
+ msg.className = 'msg msg-raw' + (tool ? ' tool-out' : '');
2291
2314
  const block = document.createElement('div');
2292
2315
  block.className = 'code-block';
2293
2316
  block.innerHTML = renderAnsiHtml(raw);
@@ -2714,6 +2737,7 @@
2714
2737
  case 'user': {
2715
2738
  // 只有字符串 content 才可能是真人/草稿发言;claude 原生 user 事件带数组 content
2716
2739
  // (tool_result 块)不是用户说话,别渲染成气泡
2740
+ if (msg.from && msg.from === socket.id) break; // 自己发的,sendCommand 时已经画过
2717
2741
  if (msg.content && typeof msg.content === 'string') {
2718
2742
  endStream();
2719
2743
  const kind = syntheticUserKind(msg.content);
@@ -2743,6 +2767,7 @@
2743
2767
  // 避免渲染完第一段 text 后置位标志,导致同事件内 [text, tool_use, text] 的第二段被跳过
2744
2768
  const skipText = _streamedTextThisCall;
2745
2769
  for (const block of msg.message.content) {
2770
+ if (seenInReplay(block, msg.message.id)) continue; // 进会话时回放里已经画过
2746
2771
  if (block.type === 'thinking' && block.thinking) {
2747
2772
  showThinking(block.thinking);
2748
2773
  } else if (block.type === 'text' && block.text) {
@@ -2761,7 +2786,7 @@
2761
2786
  }
2762
2787
  } else if (block.type === 'tool_result') {
2763
2788
  const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
2764
- if (!subaTryComplete(block.tool_use_id, content)) addRawOutput(content);
2789
+ if (!subaTryComplete(block.tool_use_id, content)) addRawOutput(content, true);
2765
2790
  }
2766
2791
  }
2767
2792
  }
@@ -2841,11 +2866,11 @@
2841
2866
  // 独立工具结果消息。同时作为 call 边界兜底:如果某适配器没发 message_start,
2842
2867
  // 这里重置 call 标志能保证后续 assistant 文本不被前一个 call 的标志误吃
2843
2868
  _streamedTextThisCall = false;
2844
- if (msg.content) {
2869
+ if (msg.content && !seenInReplay({ type: 'tool_result', tool_use_id: msg.tool_use_id })) {
2845
2870
  const text = typeof msg.content === 'string' ? msg.content
2846
2871
  : Array.isArray(msg.content) ? msg.content.map(b => b.text || '').join('\n')
2847
2872
  : JSON.stringify(msg.content);
2848
- if (text.trim() && !subaTryComplete(msg.tool_use_id, text)) addRawOutput(text);
2873
+ if (text.trim() && !subaTryComplete(msg.tool_use_id, text)) addRawOutput(text, true);
2849
2874
  }
2850
2875
  setWorkState('working');
2851
2876
  break;
@@ -2878,6 +2903,7 @@
2878
2903
 
2879
2904
  subaResetAll();
2880
2905
  _histFingerprints.clear();
2906
+ _replayedBlocks.clear();
2881
2907
  for (const msg of history) {
2882
2908
  renderHistoryMessage(msg, chatArea);
2883
2909
  trimMessages(); scrollToBottom();
@@ -4100,6 +4126,8 @@
4100
4126
  document.getElementById('set-collapse').checked = settings.collapseTools;
4101
4127
  document.getElementById('set-thinkcollapse').checked = settings.thinkCollapsed !== false;
4102
4128
  document.getElementById('set-rawclip').checked = settings.rawClip !== false;
4129
+ document.getElementById('set-toolout').checked = !!settings.showToolOut;
4130
+ applyToolOut();
4103
4131
  syncEnterModeUI();
4104
4132
  syncSkModeUI();
4105
4133
  }
@@ -4107,6 +4135,17 @@
4107
4135
  document.getElementById('set-thinking').addEventListener('change', (e) => { settings.showThinking = e.target.checked; saveSettings(); });
4108
4136
  document.getElementById('set-collapse').addEventListener('change', (e) => { settings.collapseTools = e.target.checked; saveSettings(); });
4109
4137
  document.getElementById('set-thinkcollapse').addEventListener('change', (e) => { settings.thinkCollapsed = e.target.checked; saveSettings(); });
4138
+ // 工具输出显隐:关着时「只显示前 8 行」没意义,置灰
4139
+ function applyToolOut() {
4140
+ document.body.classList.toggle('hide-toolout', !settings.showToolOut);
4141
+ document.getElementById('row-rawclip')?.classList.toggle('off', !settings.showToolOut);
4142
+ }
4143
+ applyToolOut();
4144
+ document.getElementById('set-toolout').addEventListener('change', (e) => {
4145
+ settings.showToolOut = e.target.checked; saveSettings(); applyToolOut();
4146
+ // 刚显示出来的块要重新量一次高度(隐藏时量不出是否超过 8 行)
4147
+ if (settings.showToolOut) chatArea.querySelectorAll('.msg-raw.tool-out').forEach(m => rawClipSync(m));
4148
+ });
4110
4149
  document.getElementById('set-rawclip').addEventListener('change', (e) => {
4111
4150
  settings.rawClip = e.target.checked; saveSettings();
4112
4151
  // 已渲染的输出块立即跟随新默认(用户手动点过的块也一并重置)
@@ -6242,38 +6281,48 @@
6242
6281
  setTimeout(checkUpdate, 10 * 60 * 1000);
6243
6282
  })();
6244
6283
 
6245
- window.doUpdate = async function() {
6284
+ // 等升级落地:轮询 /api/version(重启/停服期间连不上是正常的)。
6285
+ // 成功 = 版本变了(且等于目标);失败 = 服务端报了本次升级的失败原因;10 分钟兜底超时。
6286
+ async function waitUpgrade(from, target, onTick) {
6287
+ const t0 = Date.now();
6288
+ while (Date.now() - t0 < 10 * 60 * 1000) {
6289
+ await new Promise(r => setTimeout(r, 3000));
6290
+ let v = null;
6291
+ try { const r = await fetch('/api/version', { cache: 'no-store' }); if (r.ok) v = await r.json(); } catch {}
6292
+ onTick?.(!!v, Math.round((Date.now() - t0) / 1000));
6293
+ if (!v) continue;
6294
+ if (v.current !== from && (!target || v.current === target)) return { ok: true, version: v.current };
6295
+ const last = v.upgrade?.last;
6296
+ if (!v.upgrade?.running && last && !last.ok && last.at >= t0 - 5000) return { ok: false, error: last.error };
6297
+ }
6298
+ return { ok: false, error: '等了 10 分钟仍未完成,请稍后刷新页面查看版本,或到服务器上查看 myhi log' };
6299
+ }
6300
+ window.doUpdate = async function(force) {
6246
6301
  if (!_versionInfo?.updateAvailable) return;
6247
- if (!confirm(`更新 myhi?\n当前: ${_versionInfo.current}\n最新: ${_versionInfo.latest}\n\n更新后服务将自动重启`)) return;
6302
+ if (!force && !confirm(`更新 myhi?\n当前: ${_versionInfo.current}\n最新: ${_versionInfo.latest}\n\n更新后服务将自动重启`)) return;
6248
6303
  const badge = document.getElementById('update-badge');
6249
- badge.textContent = '⬆ 更新中...';
6250
- badge.style.color = 'var(--muted)';
6304
+ const setBadge = (t, c) => { badge.textContent = t; badge.style.color = c || 'var(--muted)'; };
6305
+ setBadge('⬆ 更新中...');
6251
6306
  try {
6252
- const resp = await fetch('/api/update', { method: 'POST' });
6253
- const data = await resp.json();
6307
+ const resp = await fetch('/api/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ force: !!force }) });
6308
+ const data = await resp.json().catch(() => ({}));
6254
6309
  if (!data.ok) {
6310
+ // 有任务正在执行:说清楚会打断谁,确认后强制升级
6311
+ if (data.busy?.length) {
6312
+ if (confirm(data.error + '\n\n仍然现在升级吗?')) return doUpdate(true);
6313
+ setBadge(`⬆ ${_versionInfo.latest}`, 'var(--yellow)'); return;
6314
+ }
6255
6315
  addStatusMessage('更新失败:' + (data.error || '未知错误'));
6256
- badge.textContent = '⬆ 更新失败';
6257
- badge.style.color = 'var(--red)';
6316
+ setBadge('⬆ 更新失败', 'var(--red)');
6258
6317
  return;
6259
6318
  }
6260
6319
  addStatusMessage(data.message || '升级中...');
6261
- badge.textContent = '⬆ 重启中...';
6262
- // 等待服务重启后自动刷新
6263
- setTimeout(() => {
6264
- let retries = 0;
6265
- const poll = setInterval(async () => {
6266
- try {
6267
- const r = await fetch('/api/version');
6268
- if (r.ok) { clearInterval(poll); location.reload(); }
6269
- } catch {}
6270
- if (++retries > 30) { clearInterval(poll); badge.textContent = '⬆ 请手动刷新'; badge.style.color = 'var(--red)'; }
6271
- }, 3000);
6272
- }, 5000);
6320
+ const r = await waitUpgrade(data.from, data.target, (up, sec) => setBadge(up ? `⬆ 安装中 ${sec}s` : `⬆ 重启中 ${sec}s`));
6321
+ if (r.ok) { addStatusMessage(`已升级到 ${r.version},即将刷新`); setTimeout(() => location.reload(), 800); }
6322
+ else { addStatusMessage('更新失败:' + r.error); setBadge('⬆ 更新失败', 'var(--red)'); }
6273
6323
  } catch (e) {
6274
6324
  addStatusMessage('更新失败: ' + e.message);
6275
- badge.textContent = `⬆ ${_versionInfo.latest}`;
6276
- badge.style.color = 'var(--yellow)';
6325
+ setBadge(`⬆ ${_versionInfo.latest}`, 'var(--yellow)');
6277
6326
  }
6278
6327
  };
6279
6328
 
package/dist/index.html CHANGED
@@ -187,6 +187,13 @@
187
187
  }
188
188
  .empty-state svg { opacity: 0.3; }
189
189
  .empty-state p { font-size: 0.95rem; }
190
+ .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); }
191
+ .empty-new:hover { background: var(--accent2); }
192
+ .empty-state small { font-size: 0.72rem; opacity: 0.8; }
193
+ /* 搜索框旁的「+ 新建任务」:大屏上右下角 FAB 离内容太远容易看不到;手机上有 FAB 就够了 */
194
+ #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; }
195
+ #new-btn:hover { background: var(--accent2); }
196
+ @media (max-width: 540px) { #new-btn { display: none; } }
190
197
 
191
198
  /* ── Session card ── */
192
199
  .card {
@@ -283,6 +290,14 @@
283
290
  z-index: 20;
284
291
  }
285
292
  #fab:active { background: var(--accent2); transform: scale(0.93); }
293
+ /* 首次引导关掉后:FAB 呼吸光圈 + 气泡指一下「点这里新建任务」,只出现一次 */
294
+ #fab.hint { animation: fabPulse 1.4s ease-out infinite; }
295
+ @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); } }
296
+ #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));
297
+ 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; }
298
+ #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); }
299
+ #fab-tip.show { display: block; animation: tipIn 0.3s ease; }
300
+ @keyframes tipIn { from { opacity: 0; transform: translateX(8px); } to { opacity: 1; transform: none; } }
286
301
 
287
302
  /* 随手记 FAB:灵感常常在你不在任何会话页的时候冒出来,首页也得能记 */
288
303
  #fab-note {
@@ -604,6 +619,7 @@
604
619
  autocomplete="off" spellcheck="false"
605
620
  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
621
  <span id="search-count" style="font-size:0.72rem;color:var(--muted,var(--muted));white-space:nowrap"></span>
622
+ <button id="new-btn" onclick="openSheet()" title="新建任务(也可点右下角 +)">+ 新建任务</button>
607
623
  </div>
608
624
  <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
625
  <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 +635,16 @@
619
635
  <rect x="2" y="4" width="20" height="16" rx="2"/>
620
636
  <path d="M8 9l4 3-4 3M13 15h3"/>
621
637
  </svg>
622
- <p>还没有终端会话</p>
638
+ <p>还没有任务</p>
639
+ <button class="empty-new" onclick="openSheet()">+ 新建第一个任务</button>
640
+ <small>以后随时点右下角的 + 新建</small>
623
641
  </div>
624
642
  </div>
625
643
  </div>
626
644
 
627
645
  <!-- FAB -->
628
- <button id="fab" onclick="openSheet()">+</button>
646
+ <button id="fab" onclick="openSheet()" title="新建任务">+</button>
647
+ <div id="fab-tip" onclick="openSheet()">👉 点这里新建任务</div>
629
648
  <button id="fab-note" onclick="openNoteSheet()" title="随手记:把想法先记进某个任务的草稿箱">📥</button>
630
649
 
631
650
  <!-- 使用说明 / 快速上手(首次访问自动弹出,之后从顶栏 ❓ 打开)。桌面居中卡片,手机底部抽屉 -->
@@ -653,7 +672,7 @@
653
672
  <div class="hs-tip">📱 手机浏览器打开同一地址即可(局域网 / Tailscale),建议「添加到主屏幕」当 App 用。</div>
654
673
  <div class="hs-actions">
655
674
  <button class="hs-ghost" onclick="closeHelpSheet()">我知道了</button>
656
- <button class="hs-primary" onclick="closeHelpSheet(); openSheet();">+ 新建第一个任务</button>
675
+ <button class="hs-primary" onclick="closeHelpSheet(true); openSheet();">+ 新建第一个任务</button>
657
676
  </div>
658
677
  </div>
659
678
 
@@ -709,7 +728,7 @@
709
728
  <div class="set-group-t">这台电脑 · 所有设备生效</div>
710
729
  <div class="set-card">
711
730
  <div class="set-item inp">
712
- <div class="set-lbl">本机名称<small>显示在浏览器标签上</small></div>
731
+ <div class="set-lbl">本机显示名<small>首页 / 分屏的浏览器标签显示它,不改电脑主机名;留空用主机名</small></div>
713
732
  <div class="set-ctl">
714
733
  <input id="set-machine-name" class="set-inp" maxlength="32" autocomplete="off" data-save="saveMachineName">
715
734
  <button class="set-btn" onclick="saveMachineName()">保存</button>
@@ -1224,7 +1243,9 @@
1224
1243
  <rect x="2" y="4" width="20" height="16" rx="2"/>
1225
1244
  <path d="M8 9l4 3-4 3M13 15h3"/>
1226
1245
  </svg>
1227
- <p>还没有终端会话</p>
1246
+ <p>还没有任务</p>
1247
+ <button class="empty-new" onclick="openSheet()">+ 新建第一个任务</button>
1248
+ <small>以后随时点右下角的 + 新建</small>
1228
1249
  </div>`;
1229
1250
 
1230
1251
  function cardHTML(s) {
@@ -1422,11 +1443,21 @@
1422
1443
  document.getElementById('help-backdrop').style.display = 'block';
1423
1444
  document.getElementById('help-sheet').style.display = 'block';
1424
1445
  };
1425
- window.closeHelpSheet = function() {
1446
+ window.closeHelpSheet = function(creating) {
1426
1447
  document.getElementById('help-backdrop').style.display = 'none';
1427
1448
  document.getElementById('help-sheet').style.display = 'none';
1449
+ const first = !localStorage.getItem('myhi_onboarded');
1428
1450
  localStorage.setItem('myhi_onboarded', '1'); // 看过一次就不再自动弹
1451
+ // 第一次看完引导点的是「我知道了 / ✕」:把用户的视线引到右下角 +(大屏上它离内容很远,容易看不到)
1452
+ if (first && !creating) showFabHint();
1429
1453
  };
1454
+ function showFabHint() {
1455
+ const fab = document.getElementById('fab'), tip = document.getElementById('fab-tip');
1456
+ fab.classList.add('hint'); tip.classList.add('show');
1457
+ const done = () => { fab.classList.remove('hint'); tip.classList.remove('show'); document.removeEventListener('pointerdown', done, true); };
1458
+ setTimeout(() => document.addEventListener('pointerdown', done, true), 300); // 点任何地方(含 + 本身)就收起
1459
+ setTimeout(done, 12000);
1460
+ }
1430
1461
  // 首次访问:自动弹快速上手,带着建第一个任务
1431
1462
  if (!localStorage.getItem('myhi_onboarded') && !new URLSearchParams(location.search).get('embed')) {
1432
1463
  setTimeout(openHelpSheet, 600);