@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/chat.html CHANGED
@@ -9,7 +9,8 @@
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
+ <script src="/lib/prefs.js"></script>
13
14
  <script src="/sio-client/socket.io.min.js"></script>
14
15
  <script>
15
16
  (function(){try{
@@ -202,6 +203,8 @@
202
203
  .msg-raw:hover .raw-copy, .raw-copy.done { opacity: 1; }
203
204
  .raw-copy:hover, .cp-btn:hover { color: var(--text); border-color: var(--blue); }
204
205
  @media (hover: none) { .raw-copy { opacity: 0.85; } }
206
+ /* AI 工具的原始输出默认不显示(设置→聊天→显示 打开;终端会话的命令输出不受影响) */
207
+ body.hide-toolout .msg-raw.tool-out { display: none; }
205
208
  /* 消息里其他带框内容(代码块/表格/diff/工具详情…)同款复制按钮,见 attachCopyBtns */
206
209
  .cpw, .cp-host { position: relative; }
207
210
  .cp-btn { user-select: none; }
@@ -310,7 +313,7 @@
310
313
  /* ── 设置面板 ───────────────────────────── */
311
314
  #settings-backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 60; }
312
315
  #settings-backdrop.open { display: block; }
313
- #settings-panel { position: fixed; top: 0; right: -360px; width: 340px; max-width: 92vw; height: 100%; background: var(--surface); border-left: 1px solid var(--border); z-index: 61; transition: right 0.25s ease; overflow-y: auto; padding: 1rem; }
316
+ #settings-panel { position: fixed; top: 0; right: -360px; width: 340px; max-width: 92vw; height: 100%; background: var(--surface); border-left: 1px solid var(--border); z-index: 61; transition: right 0.25s ease; overflow-y: auto; padding: 1rem; box-sizing: border-box; }
314
317
  #settings-panel.open { right: 0; }
315
318
  .panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.6rem; font-size: 1rem; font-weight: 600; }
316
319
  .panel-close { background: none; border: none; color: var(--muted); font-size: 1.2rem; cursor: pointer; }
@@ -327,6 +330,18 @@
327
330
  .seg-row { display: flex; gap: 0.35rem; flex-wrap: wrap; }
328
331
  .seg { flex: 1; min-width: 3.2rem; background: var(--surface2); color: var(--text2); border: 1px solid var(--border); border-radius: 8px; padding: 0.45rem 0.3rem; font-size: 0.8rem; cursor: pointer; white-space: nowrap; }
329
332
  .seg.active { background: var(--purple); color: #fff; border-color: var(--purple); }
333
+ /* 设置里「名称 + 分段选择」一行 */
334
+ .set-line { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; padding: 0.45rem 0; font-size: 0.85rem; }
335
+ .set-line > span { flex: 0 1 auto; min-width: 3.5rem; }
336
+ .set-line small { display: block; font-size: 0.68rem; color: var(--muted); }
337
+ .set-line .seg-row { flex: 1 1 auto; max-width: 13.5rem; flex-wrap: nowrap; }
338
+ .set-line .seg { min-width: 0; padding: 0.38rem 0.2rem; font-size: 0.76rem; }
339
+ .set-task-hint { font-size: 0.74rem; color: var(--muted); line-height: 1.5; margin-bottom: 0.6rem; }
340
+ .set-task-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.45rem; }
341
+ .set-task-grid button { background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem 0.5rem; font-size: 0.82rem; cursor: pointer; text-align: left; }
342
+ .set-task-grid button:hover { border-color: var(--purple); }
343
+ .set-foot { font-size: 0.72rem; color: var(--muted); line-height: 1.55; margin-top: 1rem; padding-top: 0.7rem; border-top: 1px solid var(--border); }
344
+ .set-foot a { color: var(--blue); }
330
345
  .setting-toggle input[type=checkbox] { width: 36px; height: 20px; appearance: none; -webkit-appearance: none; background: var(--border); border-radius: 10px; position: relative; cursor: pointer; transition: background 0.2s; }
331
346
  .setting-toggle input[type=checkbox]:checked { background: var(--green2); }
332
347
  .setting-toggle input[type=checkbox]::after { content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: left 0.2s; }
@@ -621,33 +636,16 @@
621
636
  <div id="settings-panel">
622
637
  <div class="panel-header"><span>设置</span><button class="panel-close" onclick="closeSettings()">✕</button></div>
623
638
  <div id="settings-tabs">
624
- <button class="stab active" data-tab="look">外观</button>
625
- <button class="stab" data-tab="chat">聊天</button>
639
+ <button class="stab active" data-tab="chat">聊天</button>
626
640
  <button class="stab" data-tab="bar">快捷栏</button>
627
- <button class="stab" data-tab="more">更多</button>
641
+ <button class="stab" data-tab="task" id="stab-task">本任务</button>
628
642
  </div>
629
- <div class="setting-group" data-stab="look">
630
- <div style="font-size:0.8rem;color:var(--text2);margin:0.1rem 0 0.35rem">主题</div>
631
- <div id="seg-theme" class="seg-row">
632
- <button class="seg" data-theme="system">跟随系统</button>
633
- <button class="seg" data-theme="dark">深色</button>
634
- <button class="seg" data-theme="light">浅色</button>
635
- <button class="seg" data-theme="black">纯黑</button>
636
- </div>
637
- <div style="font-size:0.8rem;color:var(--text2);margin:0.6rem 0 0.35rem">字号</div>
638
- <div id="seg-font" class="seg-row">
639
- <button class="seg" data-font="small">小</button>
640
- <button class="seg" data-font="medium">中</button>
641
- <button class="seg" data-font="large">大</button>
642
- </div>
643
- </div>
644
- <div class="setting-group" data-stab="chat" style="display:none">
643
+ <div class="setting-group" data-stab="chat">
645
644
  <h4>显示</h4>
646
- <label class="setting-toggle"><span>解析终端输出</span><input type="checkbox" id="set-parse" checked></label>
647
- <label class="setting-toggle"><span>显示思考过程</span><input type="checkbox" id="set-thinking" checked></label>
648
- <label class="setting-toggle"><span>工具调用默认折叠</span><input type="checkbox" id="set-collapse" checked></label>
649
- <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>
645
+ <div class="set-line"><span>思考过程</span><div id="seg-think" class="seg-row"><button class="seg" data-v="hide">隐藏</button><button class="seg" data-v="fold">折叠</button><button class="seg" data-v="open">展开</button></div></div>
646
+ <div class="set-line"><span>工具调用</span><div id="seg-tools" class="seg-row"><button class="seg" data-v="fold">折叠</button><button class="seg" data-v="open">展开</button></div></div>
647
+ <div class="set-line"><span>工具输出<small>AI 执行命令/读文件的原始结果</small></span><div id="seg-toolout" class="seg-row"><button class="seg" data-v="hide">隐藏</button><button class="seg" data-v="clip">前8行</button><button class="seg" data-v="all">全部</button></div></div>
648
+ <label class="setting-toggle" id="row-parse"><span>把终端里的 Claude 输出整理成聊天样式</span><input type="checkbox" id="set-parse" checked></label>
651
649
  </div>
652
650
  <div class="setting-group" data-stab="chat" style="display:none">
653
651
  <h4>输入</h4>
@@ -671,15 +669,20 @@
671
669
  </div>
672
670
  <div id="sk-visible-list" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0.15rem 0.6rem;margin-top:0.5rem"></div>
673
671
  </div>
674
- <div class="setting-group" data-stab="more" style="display:none">
675
- <h4>全局设置</h4>
676
- <div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.45rem;line-height:1.5">本机名称、语音识别、账户等对所有任务生效的设置,统一放在首页的 ⚙ 设置里。这里的「外观 / 聊天 / 快捷栏」只影响当前这台设备。</div>
677
- <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>
678
- </div>
679
- <div class="setting-group" data-stab="more" style="display:none">
680
- <h4>服务管理</h4>
681
- <button onclick="restartMyhi()" style="width:100%;padding:0.6rem;background:var(--red2);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:0.85rem;font-weight:600;margin-top:0.1rem">重启 myhi</button>
672
+ <div class="setting-group" data-stab="task" style="display:none">
673
+ <div class="set-task-hint">这些设置只对当前任务(或它所在的项目)生效,快捷栏里隐藏了的也能从这里打开。</div>
674
+ <div class="set-task-grid">
675
+ <button onclick="closeSettings();openModelSheet()">🤖 模型</button>
676
+ <button onclick="closeSettings();openGuardSheet()">🛡 隐私护栏</button>
677
+ <button onclick="closeSettings();openSecretsSheet()">🔑 密钥</button>
678
+ <button onclick="closeSettings();openFollowUpSheet()">🔄 自动跟进</button>
679
+ <button onclick="closeSettings();showMemory()">🧠 记忆</button>
680
+ <button onclick="closeSettings();openSkillSheet()">🧩 技能</button>
681
+ <button onclick="closeSettings();openInboxSheet()">📥 草稿箱</button>
682
+ <button onclick="closeSettings();openGitSheet()">⬆ 提交代码</button>
683
+ </div>
682
684
  </div>
685
+ <div class="set-foot">这里的设置会同步到你登录的其他设备。外观(主题 / 字号)、本机显示名、语音、版本升级与重启在 <a href="/?settings=1">首页 ⚙ 设置</a>。</div>
683
686
  </div>
684
687
 
685
688
  <!-- QR弹窗 -->
@@ -1209,11 +1212,11 @@
1209
1212
  // 快捷栏:skVisible 逐按钮勾选(唯一显隐依据,null=默认全显);skMode 仅记录最接近的预设(高亮用)
1210
1213
  const SK_LABELS = { draft:'📥 草稿', folder:'📂 文件夹', model:'模型', plan:'计划', compact:'压缩', resume:'恢复', clear:'清除', secrets:'🔑 密钥', guard:'🛡 护栏', followup:'🔄 跟进', git:'提交', memory:'记忆', skills:'技能', cost:'费用' };
1211
1214
  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 };
1215
+ const DEFAULT_SETTINGS = { parseOutput: true, showThinking: true, collapseTools: true, thinkCollapsed: true, rawClip: true, showToolOut: false, enterMode: 'auto', skMode: null, skVisible: null };
1213
1216
  let settings = loadSettings();
1214
1217
 
1215
1218
  function loadSettings() { try { return { ...DEFAULT_SETTINGS, ...JSON.parse(localStorage.getItem('myhi_chat_settings')) }; } catch { return { ...DEFAULT_SETTINGS }; } }
1216
- function saveSettings() { localStorage.setItem('myhi_chat_settings', JSON.stringify(settings)); }
1219
+ function saveSettings() { localStorage.setItem('myhi_chat_settings', JSON.stringify(settings)); window.myhiPrefsPush?.(); } // 同步到其他设备
1217
1220
 
1218
1221
  // ── 外观:主题 & 字号(跨页面共用 localStorage myhi_theme / myhi_font)──
1219
1222
  const THEME_META = { dark: '#0d1117', light: '#ffffff', black: '#000000', system: '#0d1117' };
@@ -1315,6 +1318,21 @@
1315
1318
  let _historyLoading = false; // 防抖锁
1316
1319
  let _userScrolledUp = false; // 用户已主动往上翻 → 关掉 trim 防被自动删
1317
1320
  const _histFingerprints = new Set(); // 已渲染历史消息指纹(翻页重叠兜底去重,agent:history 重放时清空)
1321
+ // 回放里已画过的块(工具调用 id / 工具结果 id / 文本按 message.id)。AI 正在跑时进入会话(分屏盯任务最常见):
1322
+ // 服务端从 transcript 回放时,最新几条已经写进 transcript,紧接着同样的块又作为实时事件推过来 → 画两次。
1323
+ // 实时事件按这些 id 查重,回放过的就跳过;agent:history 重放时清空。
1324
+ const _replayedBlocks = new Set();
1325
+ function blockKey(block, msgId) {
1326
+ if (block.type === 'tool_use' && block.id) return 'tu:' + block.id;
1327
+ if (block.type === 'tool_result' && block.tool_use_id) return 'tr:' + block.tool_use_id;
1328
+ if (block.type === 'text' && block.text && msgId) return 'tx:' + msgId + ':' + block.text.slice(0, 80);
1329
+ return null;
1330
+ }
1331
+ // 实时块是否在回放里画过(画过返回 true,调用方跳过)
1332
+ function seenInReplay(block, msgId) {
1333
+ const k = blockKey(block, msgId);
1334
+ return !!k && _replayedBlocks.has(k);
1335
+ }
1318
1336
 
1319
1337
  // 命令历史
1320
1338
  const cmdHistory = [];
@@ -2080,6 +2098,9 @@
2080
2098
  }
2081
2099
 
2082
2100
  function renderHistoryMessage(msg, parent) {
2101
+ if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
2102
+ for (const b of msg.message.content) { const k = blockKey(b, msg.message.id); if (k) _replayedBlocks.add(k); }
2103
+ } else if (msg.type === 'tool_result' && msg.id) _replayedBlocks.add('tr:' + msg.id);
2083
2104
  if (subaRoute(msg)) return; // 子代理内部历史消息归入对应面板(_history 兜底路径才有)
2084
2105
  const fp = histFingerprint(msg);
2085
2106
  if (fp) {
@@ -2094,7 +2115,7 @@
2094
2115
  if (!text) return;
2095
2116
  // 与 live 一致:工具输出 = 工具卡后面的独立 raw 代码块(addRawOutput 同款)
2096
2117
  const el = document.createElement('div');
2097
- el.className = 'msg msg-raw';
2118
+ el.className = 'msg msg-raw tool-out';
2098
2119
  const block = document.createElement('div');
2099
2120
  block.className = 'code-block';
2100
2121
  block.innerHTML = renderAnsiHtml(text);
@@ -2268,13 +2289,14 @@
2268
2289
  }
2269
2290
  }
2270
2291
 
2271
- function addRawOutput(raw) {
2292
+ // tool=true:AI 工具的输出(受「显示工具输出内容」开关控制);否则是终端会话的命令输出,始终显示
2293
+ function addRawOutput(raw, tool = false) {
2272
2294
  endStream(); endToolGroup();
2273
2295
  removeThinking();
2274
2296
  if (!stripAnsi(raw).trim()) return;
2275
- // 合并到上一个 raw 输出块(避免 DOM 碎片化)
2297
+ // 合并到上一个同类 raw 输出块(避免 DOM 碎片化)
2276
2298
  const last = chatArea.lastElementChild;
2277
- if (last?.classList.contains('msg-raw')) {
2299
+ if (last?.classList.contains('msg-raw') && last.classList.contains('tool-out') === tool) {
2278
2300
  const block = last.querySelector('.code-block');
2279
2301
  if (block) {
2280
2302
  block.innerHTML += renderAnsiHtml(raw);
@@ -2287,7 +2309,7 @@
2287
2309
  }
2288
2310
  }
2289
2311
  const msg = document.createElement('div');
2290
- msg.className = 'msg msg-raw';
2312
+ msg.className = 'msg msg-raw' + (tool ? ' tool-out' : '');
2291
2313
  const block = document.createElement('div');
2292
2314
  block.className = 'code-block';
2293
2315
  block.innerHTML = renderAnsiHtml(raw);
@@ -2714,6 +2736,7 @@
2714
2736
  case 'user': {
2715
2737
  // 只有字符串 content 才可能是真人/草稿发言;claude 原生 user 事件带数组 content
2716
2738
  // (tool_result 块)不是用户说话,别渲染成气泡
2739
+ if (msg.from && msg.from === socket.id) break; // 自己发的,sendCommand 时已经画过
2717
2740
  if (msg.content && typeof msg.content === 'string') {
2718
2741
  endStream();
2719
2742
  const kind = syntheticUserKind(msg.content);
@@ -2743,6 +2766,7 @@
2743
2766
  // 避免渲染完第一段 text 后置位标志,导致同事件内 [text, tool_use, text] 的第二段被跳过
2744
2767
  const skipText = _streamedTextThisCall;
2745
2768
  for (const block of msg.message.content) {
2769
+ if (seenInReplay(block, msg.message.id)) continue; // 进会话时回放里已经画过
2746
2770
  if (block.type === 'thinking' && block.thinking) {
2747
2771
  showThinking(block.thinking);
2748
2772
  } else if (block.type === 'text' && block.text) {
@@ -2761,7 +2785,7 @@
2761
2785
  }
2762
2786
  } else if (block.type === 'tool_result') {
2763
2787
  const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
2764
- if (!subaTryComplete(block.tool_use_id, content)) addRawOutput(content);
2788
+ if (!subaTryComplete(block.tool_use_id, content)) addRawOutput(content, true);
2765
2789
  }
2766
2790
  }
2767
2791
  }
@@ -2841,11 +2865,11 @@
2841
2865
  // 独立工具结果消息。同时作为 call 边界兜底:如果某适配器没发 message_start,
2842
2866
  // 这里重置 call 标志能保证后续 assistant 文本不被前一个 call 的标志误吃
2843
2867
  _streamedTextThisCall = false;
2844
- if (msg.content) {
2868
+ if (msg.content && !seenInReplay({ type: 'tool_result', tool_use_id: msg.tool_use_id })) {
2845
2869
  const text = typeof msg.content === 'string' ? msg.content
2846
2870
  : Array.isArray(msg.content) ? msg.content.map(b => b.text || '').join('\n')
2847
2871
  : JSON.stringify(msg.content);
2848
- if (text.trim() && !subaTryComplete(msg.tool_use_id, text)) addRawOutput(text);
2872
+ if (text.trim() && !subaTryComplete(msg.tool_use_id, text)) addRawOutput(text, true);
2849
2873
  }
2850
2874
  setWorkState('working');
2851
2875
  break;
@@ -2878,6 +2902,7 @@
2878
2902
 
2879
2903
  subaResetAll();
2880
2904
  _histFingerprints.clear();
2905
+ _replayedBlocks.clear();
2881
2906
  for (const msg of history) {
2882
2907
  renderHistoryMessage(msg, chatArea);
2883
2908
  trimMessages(); scrollToBottom();
@@ -3559,7 +3584,8 @@
3559
3584
  overlay.appendChild(wrap);
3560
3585
  };
3561
3586
 
3562
- 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'); };
3587
+ window.openSettings = function() { document.getElementById('settings-backdrop').classList.add('open'); document.getElementById('settings-panel').classList.add('open'); syncSettingsUI(); const t0 = localStorage.getItem('myhi_settings_tab');
3588
+ applySettingsTab(['bar', 'task'].includes(t0) && !(t0 === 'task' && currentSession?.mode !== 'agent') ? t0 : 'chat'); };
3563
3589
  // 设置抽屉分类页签(1.3.174:一屏只看一类,不再长条堆叠)
3564
3590
  function applySettingsTab(tab) {
3565
3591
  document.querySelectorAll('#settings-tabs .stab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
@@ -4096,22 +4122,51 @@
4096
4122
  function syncSettingsUI() {
4097
4123
  syncAppearanceUI();
4098
4124
  document.getElementById('set-parse').checked = settings.parseOutput;
4099
- document.getElementById('set-thinking').checked = settings.showThinking;
4100
- document.getElementById('set-collapse').checked = settings.collapseTools;
4101
- document.getElementById('set-thinkcollapse').checked = settings.thinkCollapsed !== false;
4102
- document.getElementById('set-rawclip').checked = settings.rawClip !== false;
4125
+ // 「整理终端输出」只对终端模式的 Claude 会话有用;「本任务」入口只对 AI 会话有用
4126
+ const isAgent = currentSession?.mode === 'agent';
4127
+ document.getElementById('row-parse').style.display = isAgent ? 'none' : '';
4128
+ document.getElementById('stab-task').style.display = isAgent ? '' : 'none';
4129
+ syncDisplaySegs();
4130
+ applyToolOut();
4103
4131
  syncEnterModeUI();
4104
4132
  syncSkModeUI();
4105
4133
  }
4106
4134
  document.getElementById('set-parse').addEventListener('change', (e) => { settings.parseOutput = e.target.checked; saveSettings(); });
4107
- document.getElementById('set-thinking').addEventListener('change', (e) => { settings.showThinking = e.target.checked; saveSettings(); });
4108
- document.getElementById('set-collapse').addEventListener('change', (e) => { settings.collapseTools = e.target.checked; saveSettings(); });
4109
- document.getElementById('set-thinkcollapse').addEventListener('change', (e) => { settings.thinkCollapsed = e.target.checked; saveSettings(); });
4110
- document.getElementById('set-rawclip').addEventListener('change', (e) => {
4111
- settings.rawClip = e.target.checked; saveSettings();
4112
- // 已渲染的输出块立即跟随新默认(用户手动点过的块也一并重置)
4113
- chatArea.querySelectorAll('.msg-raw').forEach(m => { delete m.dataset.open; rawClipSync(m); });
4114
- });
4135
+ // AI 工具输出显隐(body.hide-toolout 藏掉 .msg-raw.tool-out;终端会话的命令输出不受影响)
4136
+ function applyToolOut() { document.body.classList.toggle('hide-toolout', !settings.showToolOut); }
4137
+ applyToolOut();
4138
+ // 显示设置:三组分段选择,各对应原来的一两个开关
4139
+ // 思考过程 隐藏/折叠/展开 = showThinking + thinkCollapsed;工具调用 折叠/展开 = collapseTools;
4140
+ // 工具输出 隐藏/前 8 行/全部 = showToolOut + rawClip
4141
+ function displayState() {
4142
+ return {
4143
+ think: !settings.showThinking ? 'hide' : (settings.thinkCollapsed !== false ? 'fold' : 'open'),
4144
+ tools: settings.collapseTools ? 'fold' : 'open',
4145
+ toolout: !settings.showToolOut ? 'hide' : (settings.rawClip !== false ? 'clip' : 'all'),
4146
+ };
4147
+ }
4148
+ function syncDisplaySegs() {
4149
+ const st = displayState();
4150
+ for (const [id, key] of [['seg-think', 'think'], ['seg-tools', 'tools'], ['seg-toolout', 'toolout']]) {
4151
+ document.querySelectorAll('#' + id + ' .seg').forEach(b => b.classList.toggle('active', b.dataset.v === st[key]));
4152
+ }
4153
+ }
4154
+ document.querySelectorAll('#seg-think .seg').forEach(b => b.addEventListener('click', () => {
4155
+ settings.showThinking = b.dataset.v !== 'hide';
4156
+ if (b.dataset.v !== 'hide') settings.thinkCollapsed = b.dataset.v === 'fold';
4157
+ saveSettings(); syncDisplaySegs();
4158
+ }));
4159
+ document.querySelectorAll('#seg-tools .seg').forEach(b => b.addEventListener('click', () => {
4160
+ settings.collapseTools = b.dataset.v === 'fold'; saveSettings(); syncDisplaySegs();
4161
+ }));
4162
+ document.querySelectorAll('#seg-toolout .seg').forEach(b => b.addEventListener('click', () => {
4163
+ const was = displayState().toolout;
4164
+ settings.showToolOut = b.dataset.v !== 'hide';
4165
+ if (b.dataset.v !== 'hide') settings.rawClip = b.dataset.v === 'clip';
4166
+ saveSettings(); syncDisplaySegs(); applyToolOut();
4167
+ // 显示方式变了:已渲染的输出块按新默认重新量(隐藏时量不出高度;用户手动点过的展开/收起也重置)
4168
+ if (settings.showToolOut && was !== b.dataset.v) chatArea.querySelectorAll('.msg-raw.tool-out').forEach(m => { delete m.dataset.open; rawClipSync(m); });
4169
+ }));
4115
4170
 
4116
4171
  window.restartMyhi = async function() {
4117
4172
  if (!confirm('确认重启 myhi 服务?\n重启期间连接会短暂中断,页面将自动重连。')) return;
@@ -6242,38 +6297,48 @@
6242
6297
  setTimeout(checkUpdate, 10 * 60 * 1000);
6243
6298
  })();
6244
6299
 
6245
- window.doUpdate = async function() {
6300
+ // 等升级落地:轮询 /api/version(重启/停服期间连不上是正常的)。
6301
+ // 成功 = 版本变了(且等于目标);失败 = 服务端报了本次升级的失败原因;10 分钟兜底超时。
6302
+ async function waitUpgrade(from, target, onTick) {
6303
+ const t0 = Date.now();
6304
+ while (Date.now() - t0 < 10 * 60 * 1000) {
6305
+ await new Promise(r => setTimeout(r, 3000));
6306
+ let v = null;
6307
+ try { const r = await fetch('/api/version', { cache: 'no-store' }); if (r.ok) v = await r.json(); } catch {}
6308
+ onTick?.(!!v, Math.round((Date.now() - t0) / 1000));
6309
+ if (!v) continue;
6310
+ if (v.current !== from && (!target || v.current === target)) return { ok: true, version: v.current };
6311
+ const last = v.upgrade?.last;
6312
+ if (!v.upgrade?.running && last && !last.ok && last.at >= t0 - 5000) return { ok: false, error: last.error };
6313
+ }
6314
+ return { ok: false, error: '等了 10 分钟仍未完成,请稍后刷新页面查看版本,或到服务器上查看 myhi log' };
6315
+ }
6316
+ window.doUpdate = async function(force) {
6246
6317
  if (!_versionInfo?.updateAvailable) return;
6247
- if (!confirm(`更新 myhi?\n当前: ${_versionInfo.current}\n最新: ${_versionInfo.latest}\n\n更新后服务将自动重启`)) return;
6318
+ if (!force && !confirm(`更新 myhi?\n当前: ${_versionInfo.current}\n最新: ${_versionInfo.latest}\n\n更新后服务将自动重启`)) return;
6248
6319
  const badge = document.getElementById('update-badge');
6249
- badge.textContent = '⬆ 更新中...';
6250
- badge.style.color = 'var(--muted)';
6320
+ const setBadge = (t, c) => { badge.textContent = t; badge.style.color = c || 'var(--muted)'; };
6321
+ setBadge('⬆ 更新中...');
6251
6322
  try {
6252
- const resp = await fetch('/api/update', { method: 'POST' });
6253
- const data = await resp.json();
6323
+ const resp = await fetch('/api/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ force: !!force }) });
6324
+ const data = await resp.json().catch(() => ({}));
6254
6325
  if (!data.ok) {
6326
+ // 有任务正在执行:说清楚会打断谁,确认后强制升级
6327
+ if (data.busy?.length) {
6328
+ if (confirm(data.error + '\n\n仍然现在升级吗?')) return doUpdate(true);
6329
+ setBadge(`⬆ ${_versionInfo.latest}`, 'var(--yellow)'); return;
6330
+ }
6255
6331
  addStatusMessage('更新失败:' + (data.error || '未知错误'));
6256
- badge.textContent = '⬆ 更新失败';
6257
- badge.style.color = 'var(--red)';
6332
+ setBadge('⬆ 更新失败', 'var(--red)');
6258
6333
  return;
6259
6334
  }
6260
6335
  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);
6336
+ const r = await waitUpgrade(data.from, data.target, (up, sec) => setBadge(up ? `⬆ 安装中 ${sec}s` : `⬆ 重启中 ${sec}s`));
6337
+ if (r.ok) { addStatusMessage(`已升级到 ${r.version},即将刷新`); setTimeout(() => location.reload(), 800); }
6338
+ else { addStatusMessage('更新失败:' + r.error); setBadge('⬆ 更新失败', 'var(--red)'); }
6273
6339
  } catch (e) {
6274
6340
  addStatusMessage('更新失败: ' + e.message);
6275
- badge.textContent = `⬆ ${_versionInfo.latest}`;
6276
- badge.style.color = 'var(--yellow)';
6341
+ setBadge(`⬆ ${_versionInfo.latest}`, 'var(--yellow)');
6277
6342
  }
6278
6343
  };
6279
6344