@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/bin/daemon.js +196 -170
- package/bin/install-check.js +25 -0
- package/dist/admin.html +144 -27
- package/dist/chat.html +144 -79
- package/dist/index.html +171 -16
- package/dist/index.js +4 -4
- package/dist/index.min.js +14 -18
- package/dist/lib/machine.js +11 -1
- package/dist/lib/prefs.js +51 -0
- package/dist/terminal.html +1 -1
- package/package.json +1 -1
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="
|
|
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="
|
|
641
|
+
<button class="stab" data-tab="task" id="stab-task">本任务</button>
|
|
628
642
|
</div>
|
|
629
|
-
<div class="setting-group" data-stab="
|
|
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
|
-
<
|
|
647
|
-
<
|
|
648
|
-
<
|
|
649
|
-
<label class="setting-toggle"><span
|
|
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="
|
|
675
|
-
<
|
|
676
|
-
<div
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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();
|
|
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
|
-
|
|
4100
|
-
|
|
4101
|
-
document.getElementById('
|
|
4102
|
-
document.getElementById('
|
|
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
|
-
|
|
4108
|
-
document.
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6257
|
-
badge.style.color = 'var(--red)';
|
|
6332
|
+
setBadge('⬆ 更新失败', 'var(--red)');
|
|
6258
6333
|
return;
|
|
6259
6334
|
}
|
|
6260
6335
|
addStatusMessage(data.message || '升级中...');
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
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
|
-
|
|
6276
|
-
badge.style.color = 'var(--yellow)';
|
|
6341
|
+
setBadge(`⬆ ${_versionInfo.latest}`, 'var(--yellow)');
|
|
6277
6342
|
}
|
|
6278
6343
|
};
|
|
6279
6344
|
|