@wendongfly/myhi 1.3.175 → 1.3.179

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,6 +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
13
  <script src="/sio-client/socket.io.min.js"></script>
13
14
  <script>
14
15
  (function(){try{
@@ -189,6 +190,17 @@
189
190
 
190
191
  /* 原始输出(降级) */
191
192
  .msg-raw .code-block { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem 0.75rem; font-family: 'SF Mono', 'Consolas', monospace; font-size: 0.8rem; line-height: 1.35; white-space: pre-wrap; word-break: break-all; overflow-x: auto; }
193
+ /* 工具输出局部显示:只露前 8 行 + 底部渐隐,按钮展开/收起(设置→聊天可改默认全部) */
194
+ .msg-raw.clip .code-block { max-height: calc(0.8rem * 1.35 * 8 + 1.2rem); overflow: hidden; position: relative; }
195
+ .msg-raw.clip .code-block::after { content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2.2em; background: linear-gradient(transparent, var(--surface)); pointer-events: none; }
196
+ .raw-toggle { display: block; margin: 0.25rem 0 0 auto; font-size: 0.68rem; color: var(--muted); background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: 0.1rem 0.5rem; cursor: pointer; white-space: nowrap; }
197
+ .raw-toggle:hover { color: var(--text); border-color: var(--blue); }
198
+ /* 工具输出复制:右上角,桌面端悬停才现身,触屏常显 */
199
+ .msg-raw { position: relative; }
200
+ .raw-copy { position: absolute; top: 0.35rem; right: 0.35rem; z-index: 1; font-size: 0.68rem; color: var(--muted); background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: 0.1rem 0.45rem; cursor: pointer; opacity: 0; transition: opacity 0.15s; }
201
+ .msg-raw:hover .raw-copy, .raw-copy.done { opacity: 1; }
202
+ .raw-copy:hover { color: var(--text); border-color: var(--blue); }
203
+ @media (hover: none) { .raw-copy { opacity: 0.85; } }
192
204
 
193
205
  /* 历史和状态 */
194
206
  .msg-history .code-block { background: var(--bg); border: 1px solid var(--surface2); border-radius: 8px; padding: 0.6rem 0.75rem; font-family: 'SF Mono', 'Consolas', monospace; font-size: 0.75rem; line-height: 1.3; white-space: pre-wrap; word-break: break-all; color: var(--muted); max-height: 300px; overflow-y: auto; }
@@ -456,6 +468,7 @@
456
468
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
457
469
  </button>
458
470
  <button class="top-btn" id="ctx-chip" onclick="openCtxSheet()" title="会话数据:上下文水位 / 成本 / token 明细" style="display:none;width:auto;padding:0 0.5rem;white-space:nowrap;font-size:0.7rem;color:var(--muted)">◈ --</button>
471
+ <button class="top-btn" onclick="openChatHelp()" title="使用说明" style="font-size:0.7rem;color:var(--muted)">?</button>
459
472
  <button class="top-btn" onclick="openFilePanel()" title="文件" style="font-size:0.7rem;color:var(--muted)">文件</button>
460
473
  <button class="top-btn" onclick="openSwitchSheet()" title="切换会话" style="font-size:0.7rem;color:var(--muted)">切换</button>
461
474
  <button class="top-btn" onclick="goBack()" title="返回">
@@ -625,6 +638,7 @@
625
638
  <label class="setting-toggle"><span>显示思考过程</span><input type="checkbox" id="set-thinking" checked></label>
626
639
  <label class="setting-toggle"><span>工具调用默认折叠</span><input type="checkbox" id="set-collapse" checked></label>
627
640
  <label class="setting-toggle"><span>思考过程默认折叠</span><input type="checkbox" id="set-thinkcollapse" checked></label>
641
+ <label class="setting-toggle"><span>工具输出只显示前 8 行(可展开)</span><input type="checkbox" id="set-rawclip" checked></label>
628
642
  </div>
629
643
  <div class="setting-group" data-stab="chat" style="display:none">
630
644
  <h4>输入</h4>
@@ -648,6 +662,25 @@
648
662
  </div>
649
663
  <div id="sk-visible-list" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0.15rem 0.6rem;margin-top:0.5rem"></div>
650
664
  </div>
665
+ <div class="setting-group" data-stab="more" style="display:none">
666
+ <h4>本机名称</h4>
667
+ <div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.35rem;line-height:1.5">多台电脑都跑 myhi 时用来区分:浏览器标题以它开头,标签图标按名字自动配色。留空 = 用电脑主机名。</div>
668
+ <div style="display:flex;gap:0.4rem">
669
+ <input id="set-machine-name" class="slash-inp" maxlength="32" autocomplete="off" style="flex:1;font-size:0.82rem">
670
+ <button onclick="saveMachineName()" style="background:var(--blue2);color:#fff;border:none;border-radius:8px;padding:0.4rem 0.8rem;font-size:0.78rem;cursor:pointer;white-space:nowrap">保存</button>
671
+ </div>
672
+ <div id="set-machine-status" style="font-size:0.72rem;min-height:1.2em;margin-top:0.25rem;color:var(--muted)"></div>
673
+ </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.35rem;line-height:1.5">允许 @ 补全引用任务目录之外的目录(如公共资料库),命中以绝对路径插入、AI 直接按绝对路径读取。</div>
677
+ <div id="atroots-list"></div>
678
+ <div style="display:flex;gap:0.4rem;margin-top:0.4rem">
679
+ <input id="atroot-new" class="slash-inp" placeholder="例如 D:\资料库" autocomplete="off" style="flex:1;font-size:0.82rem">
680
+ <button onclick="addAtRoot()" style="background:var(--blue2);color:#fff;border:none;border-radius:8px;padding:0.4rem 0.8rem;font-size:0.78rem;cursor:pointer;white-space:nowrap">添加</button>
681
+ </div>
682
+ <div id="atroot-status" style="font-size:0.72rem;min-height:1.2em;margin-top:0.25rem;color:var(--muted)"></div>
683
+ </div>
651
684
  <div class="setting-group" data-stab="more" style="display:none">
652
685
  <h4>语音识别 (ASR)</h4>
653
686
  <div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.35rem">SenseVoice 或兼容服务的地址,留空表示禁用语音</div>
@@ -1188,7 +1221,7 @@
1188
1221
  // 快捷栏:skVisible 逐按钮勾选(唯一显隐依据,null=默认全显);skMode 仅记录最接近的预设(高亮用)
1189
1222
  const SK_LABELS = { draft:'📥 草稿', folder:'📂 文件夹', model:'模型', plan:'计划', compact:'压缩', resume:'恢复', clear:'清除', secrets:'🔑 密钥', guard:'🛡 护栏', followup:'🔄 跟进', git:'提交', memory:'记忆', skills:'技能', cost:'费用' };
1190
1223
  const SK_NORMAL_PRESET = ['draft', 'folder', 'plan', 'clear', 'guard', 'memory', 'skills'];
1191
- const DEFAULT_SETTINGS = { parseOutput: true, showThinking: true, collapseTools: true, thinkCollapsed: true, enterMode: 'auto', skMode: null, skVisible: null };
1224
+ const DEFAULT_SETTINGS = { parseOutput: true, showThinking: true, collapseTools: true, thinkCollapsed: true, rawClip: true, enterMode: 'auto', skMode: null, skVisible: null };
1192
1225
  let settings = loadSettings();
1193
1226
 
1194
1227
  function loadSettings() { try { return { ...DEFAULT_SETTINGS, ...JSON.parse(localStorage.getItem('myhi_chat_settings')) }; } catch { return { ...DEFAULT_SETTINGS }; } }
@@ -1283,6 +1316,36 @@
1283
1316
  updateEnterHint();
1284
1317
  syncSkModeUI();
1285
1318
 
1319
+ // ── @ 引用范围(项目外目录):客户自助配置,@ 补全可引用这些目录 ──
1320
+ let _atRoots = null;
1321
+ function renderAtRoots() {
1322
+ const el = document.getElementById('atroots-list');
1323
+ if (!el) return;
1324
+ el.innerHTML = (_atRoots || []).length
1325
+ ? _atRoots.map((r, i) => `<div style="display:flex;align-items:center;gap:0.4rem;padding:0.25rem 0;font-size:0.78rem"><span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtml(r)}">📁 ${escHtml(r)}</span><button class="dr-btn" onclick="delAtRoot(${i})">删</button></div>`).join('')
1326
+ : '<div style="font-size:0.72rem;color:var(--muted)">(未配置——@ 只搜任务目录)</div>';
1327
+ }
1328
+ function loadAtRoots() {
1329
+ return fetch('/api/atroots').then(r => r.json()).then(d => { _atRoots = d.roots || []; renderAtRoots(); }).catch(() => {});
1330
+ }
1331
+ async function saveAtRoots(roots) {
1332
+ const st = document.getElementById('atroot-status');
1333
+ try {
1334
+ const r = await fetch('/api/atroots', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ roots }) });
1335
+ const d = await r.json();
1336
+ if (!d.ok) { st.textContent = '保存失败:' + (d.error || ''); return false; }
1337
+ _atRoots = d.roots; renderAtRoots(); st.textContent = '已保存';
1338
+ return true;
1339
+ } catch (e) { st.textContent = '保存出错:' + e.message; return false; }
1340
+ }
1341
+ window.addAtRoot = function() {
1342
+ const inp = document.getElementById('atroot-new');
1343
+ const v = inp.value.trim();
1344
+ if (!v) return;
1345
+ saveAtRoots([...(_atRoots || []), v]).then(ok => { if (ok) inp.value = ''; });
1346
+ };
1347
+ window.delAtRoot = function(i) { saveAtRoots((_atRoots || []).filter((_, j) => j !== i)); };
1348
+
1286
1349
  // 输出累积
1287
1350
  let outputBuffer = '';
1288
1351
  let outputTimer = null;
@@ -2079,6 +2142,7 @@
2079
2142
  block.innerHTML = renderAnsiHtml(text);
2080
2143
  el.appendChild(block);
2081
2144
  parent.appendChild(el);
2145
+ rawClipSync(el);
2082
2146
  return;
2083
2147
  }
2084
2148
 
@@ -2259,6 +2323,7 @@
2259
2323
  // 限制单块行数
2260
2324
  const lines = block.innerHTML.split('\n');
2261
2325
  if (lines.length > 300) block.innerHTML = lines.slice(-200).join('\n');
2326
+ rawClipSync(last);
2262
2327
  scrollToBottom();
2263
2328
  return;
2264
2329
  }
@@ -2270,10 +2335,68 @@
2270
2335
  block.innerHTML = renderAnsiHtml(raw);
2271
2336
  msg.appendChild(block);
2272
2337
  chatArea.appendChild(msg);
2338
+ rawClipSync(msg);
2273
2339
  trimMessages();
2274
2340
  scrollToBottom();
2275
2341
  }
2276
2342
 
2343
+ // 复制到剪贴板。HTTP 访问(非安全上下文)没有 navigator.clipboard,退回 textarea + execCommand
2344
+ async function copyText(text) {
2345
+ try {
2346
+ if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); return true; }
2347
+ } catch {}
2348
+ const ta = document.createElement('textarea');
2349
+ ta.value = text;
2350
+ ta.setAttribute('readonly', '');
2351
+ ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none';
2352
+ document.body.appendChild(ta);
2353
+ ta.select(); ta.setSelectionRange(0, text.length); // iOS 需要 setSelectionRange
2354
+ let ok = false;
2355
+ try { ok = document.execCommand('copy'); } catch {}
2356
+ ta.remove();
2357
+ return ok;
2358
+ }
2359
+
2360
+ // 工具输出局部/全部显示。默认跟设置 rawClip;用户点过按钮的块记在 dataset.open,内容追加时保持。
2361
+ // 只有内容真的超出 8 行高度才出按钮;回放时元素可能还在 fragment 里没上屏,等上屏再量。
2362
+ function rawClipSync(msg, tries = 0) {
2363
+ if (!msg.isConnected) { if (tries < 30) requestAnimationFrame(() => rawClipSync(msg, tries + 1)); return; }
2364
+ const block = msg.querySelector('.code-block');
2365
+ if (!block) return;
2366
+ if (!msg.querySelector('.raw-copy')) {
2367
+ const cp = document.createElement('button');
2368
+ cp.className = 'raw-copy';
2369
+ cp.textContent = '复制';
2370
+ cp.title = '复制全部输出';
2371
+ cp.onclick = async () => {
2372
+ const ok = await copyText(block.textContent.replace(/\n+$/, ''));
2373
+ cp.textContent = ok ? '已复制' : '复制失败';
2374
+ cp.classList.add('done');
2375
+ clearTimeout(cp._t);
2376
+ cp._t = setTimeout(() => { cp.textContent = '复制'; cp.classList.remove('done'); }, 1500);
2377
+ };
2378
+ msg.appendChild(cp);
2379
+ }
2380
+ const clip = msg.dataset.open ? msg.dataset.open !== '1' : settings.rawClip !== false;
2381
+ msg.classList.add('clip'); // 先按局部高度量一次,判断是否超出
2382
+ const overflow = block.scrollHeight > block.clientHeight + 2;
2383
+ msg.classList.toggle('clip', clip && overflow);
2384
+ let btn = msg.querySelector('.raw-toggle');
2385
+ if (!overflow) { btn?.remove(); return; }
2386
+ if (!btn) {
2387
+ btn = document.createElement('button');
2388
+ btn.className = 'raw-toggle';
2389
+ btn.onclick = () => {
2390
+ msg.dataset.open = msg.classList.contains('clip') ? '1' : '0';
2391
+ rawClipSync(msg);
2392
+ if (msg.dataset.open === '0') msg.scrollIntoView({ block: 'nearest' }); // 收起后别让视口停在空白处
2393
+ };
2394
+ msg.appendChild(btn);
2395
+ }
2396
+ const n = block.textContent.replace(/\n+$/, '').split('\n').length;
2397
+ btn.textContent = msg.classList.contains('clip') ? `展开全部(共 ${n} 行)` : '收起';
2398
+ }
2399
+
2277
2400
  function addHistoryBlock(raw) {
2278
2401
  if (!stripAnsi(raw).trim()) return;
2279
2402
  let lines = raw.split('\n');
@@ -3152,7 +3275,7 @@
3152
3275
  const dir = cut >= 0 ? path.slice(0, cut + 1) : '';
3153
3276
  const name = cut >= 0 ? path.slice(cut + 1) : path;
3154
3277
  const ico = f.dir ? '📁' : (isEditableFile(name) ? '📝' : '📄');
3155
- const hint = f.dir ? '文件夹' : '引用';
3278
+ const hint = f.dir ? (f.abs ? '项目外目录' : '文件夹') : (f.abs ? '项目外' : '引用');
3156
3279
  return `<div class="mi ${i === _mention.idx ? 'on' : ''}" data-i="${i}"><span class="m-ico">${ico}</span><span class="m-dir">${escHtml(dir)}</span><span class="m-name">${escHtml(name)}</span><span class="m-hint">${hint}</span></div>`;
3157
3280
  }
3158
3281
 
@@ -3209,10 +3332,22 @@
3209
3332
  cmdInput.addEventListener('input', (e) => { if (!e.isComposing) mentionUpdate(); });
3210
3333
  cmdInput.addEventListener('keyup', (e) => { if (!['ArrowUp','ArrowDown','Escape','Enter'].includes(e.key)) mentionUpdate(); });
3211
3334
  cmdInput.addEventListener('blur', () => setTimeout(mentionClose, 120)); // 等候选中的 pointerdown 先落地
3335
+ // 触摸滚动 vs 点击区分:pointerdown 只防 textarea 失焦并记起点,
3336
+ // pointerup 时位移 <8px 才选中——否则手机上滚清单的手势一起就误引用(1.3.176 用户反馈)
3337
+ let _mentionDown = null;
3212
3338
  _mentionBox.addEventListener('pointerdown', (e) => {
3213
3339
  const row = e.target.closest('.mi[data-i]');
3214
- if (row) { e.preventDefault(); mentionPick(_mention.items[Number(row.dataset.i)]); }
3340
+ if (row) e.preventDefault();
3341
+ _mentionDown = row ? { x: e.clientX, y: e.clientY, row } : null;
3342
+ });
3343
+ _mentionBox.addEventListener('pointerup', (e) => {
3344
+ const d = _mentionDown; _mentionDown = null;
3345
+ if (!d) return;
3346
+ if (Math.abs(e.clientX - d.x) >= 8 || Math.abs(e.clientY - d.y) >= 8) return; // 滚动手势
3347
+ const row = e.pointerType === 'touch' ? d.row : e.target.closest('.mi[data-i]');
3348
+ if (row) mentionPick(_mention.items[Number(row.dataset.i)]);
3215
3349
  });
3350
+ _mentionBox.addEventListener('pointercancel', () => { _mentionDown = null; });
3216
3351
  window.addEventListener('resize', () => { if (_mention.open) mentionClose(); });
3217
3352
 
3218
3353
  cmdInput.addEventListener('keydown', (e) => {
@@ -3289,7 +3424,34 @@
3289
3424
  }
3290
3425
 
3291
3426
  // ── 设置面板 ──────────────────────────────────
3292
- 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'); };
3427
+ // ── 会话内使用说明(精简版;完整版在首页 ❓ 帮助)──────────────
3428
+ window.openChatHelp = function() {
3429
+ overlay.innerHTML = '';
3430
+ overlay.classList.remove('hidden');
3431
+ const wrap = document.createElement('div');
3432
+ wrap.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:1rem;background:rgba(0,0,0,0.55)';
3433
+ const box = document.createElement('div');
3434
+ box.style.cssText = 'max-width:520px;background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:1.1rem;font-size:0.82rem;line-height:1.75;color:var(--text);max-height:80vh;overflow-y:auto';
3435
+ box.innerHTML = `
3436
+ <div style="font-size:1rem;font-weight:700;margin-bottom:0.5rem">💬 会话怎么用</div>
3437
+ <b>发消息</b>:像聊天一样把需求说清楚即可;触屏回车=换行,用发送键发送。<br>
3438
+ <b>让它读文件</b>:打 <b>@</b> 弹文件搜索(含设置里配的项目外目录),选中插路径;或点「文件」面板的 <b>@</b> 引用。<br>
3439
+ <b>工具/思考</b>:AI 干活的过程默认折叠,点「🔧 N 个工具调用」展开看命令与输出。<br>
3440
+ <b>AI 忙时</b>:输入框变「存草稿」,先记着干完取回;勾自动接力会自动发出。<br>
3441
+ <b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰ 可列出你发过的消息点击跳转。<br>
3442
+ <b>常用按钮</b>:模型/计划/压缩/恢复/清除/密钥/护栏/提交——普通用户可在设置→快捷栏精简。<br>
3443
+ <span style="color:var(--muted)">更多说明见首页顶栏 ❓ 帮助。</span>`;
3444
+ const closeBtn = document.createElement('button');
3445
+ closeBtn.textContent = '知道了';
3446
+ closeBtn.style.cssText = 'display:block;margin:0.8rem auto 0;background:var(--blue2);color:#fff;border:none;border-radius:8px;padding:0.5rem 1.4rem;font-size:0.85rem;cursor:pointer';
3447
+ closeBtn.onclick = () => overlay.classList.add('hidden');
3448
+ box.appendChild(closeBtn);
3449
+ wrap.appendChild(box);
3450
+ wrap.onclick = (e) => { if (e.target === wrap) overlay.classList.add('hidden'); };
3451
+ overlay.appendChild(wrap);
3452
+ };
3453
+
3454
+ window.openSettings = function() { document.getElementById('settings-backdrop').classList.add('open'); document.getElementById('settings-panel').classList.add('open'); syncSettingsUI(); loadAtRoots(); applySettingsTab(localStorage.getItem('myhi_settings_tab') || 'look'); };
3293
3455
  // 设置抽屉分类页签(1.3.174:一屏只看一类,不再长条堆叠)
3294
3456
  function applySettingsTab(tab) {
3295
3457
  document.querySelectorAll('#settings-tabs .stab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
@@ -3828,17 +3990,49 @@
3828
3990
  document.getElementById('set-thinking').checked = settings.showThinking;
3829
3991
  document.getElementById('set-collapse').checked = settings.collapseTools;
3830
3992
  document.getElementById('set-thinkcollapse').checked = settings.thinkCollapsed !== false;
3993
+ document.getElementById('set-rawclip').checked = settings.rawClip !== false;
3831
3994
  syncEnterModeUI();
3832
3995
  syncSkModeUI();
3833
3996
  // 从服务端加载 ASR URL
3834
3997
  fetch('/api/server-config').then(r => r.json()).then(d => {
3835
3998
  document.getElementById('set-asr-url').value = d.asrUrl || '';
3836
3999
  }).catch(() => {});
4000
+ fetch('/api/machine-name').then(r => r.json()).then(d => {
4001
+ const inp = document.getElementById('set-machine-name');
4002
+ inp.placeholder = d.hostname || '';
4003
+ inp.value = d.name === d.hostname ? '' : (d.name || '');
4004
+ inp.disabled = !!d.fromEnv;
4005
+ document.getElementById('set-machine-status').textContent = d.fromEnv ? '已由环境变量 MYHI_NAME 指定,此处不可改' : '';
4006
+ }).catch(() => {});
3837
4007
  }
3838
4008
  document.getElementById('set-parse').addEventListener('change', (e) => { settings.parseOutput = e.target.checked; saveSettings(); });
3839
4009
  document.getElementById('set-thinking').addEventListener('change', (e) => { settings.showThinking = e.target.checked; saveSettings(); });
3840
4010
  document.getElementById('set-collapse').addEventListener('change', (e) => { settings.collapseTools = e.target.checked; saveSettings(); });
3841
4011
  document.getElementById('set-thinkcollapse').addEventListener('change', (e) => { settings.thinkCollapsed = e.target.checked; saveSettings(); });
4012
+ document.getElementById('set-rawclip').addEventListener('change', (e) => {
4013
+ settings.rawClip = e.target.checked; saveSettings();
4014
+ // 已渲染的输出块立即跟随新默认(用户手动点过的块也一并重置)
4015
+ chatArea.querySelectorAll('.msg-raw').forEach(m => { delete m.dataset.open; rawClipSync(m); });
4016
+ });
4017
+
4018
+ window.saveMachineName = async function() {
4019
+ const statusEl = document.getElementById('set-machine-status');
4020
+ try {
4021
+ const resp = await fetch('/api/machine-name', {
4022
+ method: 'POST',
4023
+ headers: { 'Content-Type': 'application/json' },
4024
+ body: JSON.stringify({ name: document.getElementById('set-machine-name').value.trim() }),
4025
+ });
4026
+ const data = await resp.json();
4027
+ if (!data.ok) throw new Error(data.error || '未知错误');
4028
+ window.myhiSetMachineName?.(data.name);
4029
+ statusEl.style.color = 'var(--green)';
4030
+ statusEl.textContent = `已保存:${data.name}(其他已打开的页面刷新后生效)`;
4031
+ } catch (e) {
4032
+ statusEl.style.color = 'var(--red)';
4033
+ statusEl.textContent = '保存失败: ' + e.message;
4034
+ }
4035
+ };
3842
4036
 
3843
4037
  window.saveAsrUrl = async function() {
3844
4038
  const url = document.getElementById('set-asr-url').value.trim();