@wendongfly/myhi 1.3.175 → 1.3.180

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; }
@@ -398,6 +410,8 @@
398
410
  .dr-text { color: var(--text); font-size: 0.82rem; line-height: 1.5; word-break: break-word; white-space: pre-wrap; }
399
411
  .dr-meta { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem 0.6rem; margin-top: 0.35rem; font-size: 0.66rem; color: var(--muted); }
400
412
  .dr-acts { display: flex; gap: 0.4rem; margin-left: auto; flex-shrink: 0; }
413
+ .atpick-row { padding: 0.42rem 0.7rem; cursor: pointer; color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
414
+ .atpick-row:hover { background: var(--surface2); color: var(--text); }
401
415
  .dr-btn { flex-shrink: 0; white-space: nowrap; padding: 0.28rem 0.7rem; font-size: 0.72rem; border: none; border-radius: 6px; background: var(--surface2); color: var(--text2); cursor: pointer; }
402
416
  .dr-btn.take { background: var(--green); color: #fff; font-weight: 600; }
403
417
  .dr-btn:active { opacity: 0.75; }
@@ -452,6 +466,7 @@
452
466
  <div id="session-title">加载中...</div>
453
467
  <div id="session-cwd"></div>
454
468
  </div>
469
+ <button class="top-btn" onclick="openChatHelp()" title="使用说明" style="font-size:0.7rem;color:var(--muted)">?</button>
455
470
  <button class="top-btn" onclick="openSettings()" title="设置">
456
471
  <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
472
  </button>
@@ -625,6 +640,7 @@
625
640
  <label class="setting-toggle"><span>显示思考过程</span><input type="checkbox" id="set-thinking" checked></label>
626
641
  <label class="setting-toggle"><span>工具调用默认折叠</span><input type="checkbox" id="set-collapse" checked></label>
627
642
  <label class="setting-toggle"><span>思考过程默认折叠</span><input type="checkbox" id="set-thinkcollapse" checked></label>
643
+ <label class="setting-toggle"><span>工具输出只显示前 8 行(可展开)</span><input type="checkbox" id="set-rawclip" checked></label>
628
644
  </div>
629
645
  <div class="setting-group" data-stab="chat" style="display:none">
630
646
  <h4>输入</h4>
@@ -648,6 +664,41 @@
648
664
  </div>
649
665
  <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
666
  </div>
667
+ <div class="setting-group" data-stab="more" style="display:none">
668
+ <h4>本机名称</h4>
669
+ <div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.35rem;line-height:1.5">多台电脑都跑 myhi 时用来区分:浏览器标题以它开头,标签图标按名字自动配色。留空 = 用电脑主机名。</div>
670
+ <div style="display:flex;gap:0.4rem">
671
+ <input id="set-machine-name" class="slash-inp" maxlength="32" autocomplete="off" style="flex:1;font-size:0.82rem">
672
+ <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>
673
+ </div>
674
+ <div id="set-machine-status" style="font-size:0.72rem;min-height:1.2em;margin-top:0.25rem;color:var(--muted)"></div>
675
+ </div>
676
+ <div class="setting-group" data-stab="more" style="display:none">
677
+ <h4>@ 引用范围(项目外)</h4>
678
+ <div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.35rem;line-height:1.5">除了任务目录,再把常用的资料文件夹(如公共资料库)加进来,打 @ 时也能搜到里面的文件。</div>
679
+ <div id="atroots-list"></div>
680
+ <button id="atpick-open" onclick="openAtPicker()" style="width:100%;margin-top:0.4rem;background:var(--blue2);color:#fff;border:none;border-radius:8px;padding:0.5rem;font-size:0.8rem;cursor:pointer">📁 选择文件夹添加…</button>
681
+ <div id="atpick" style="display:none;margin-top:0.4rem;border:1px solid var(--border);border-radius:10px;overflow:hidden">
682
+ <div id="atpick-drives" style="display:none;gap:0.3rem;padding:0.4rem 0.5rem 0;flex-wrap:wrap"></div>
683
+ <div style="display:flex;align-items:center;gap:0.35rem;padding:0.4rem 0.5rem;border-bottom:1px solid var(--border)">
684
+ <button id="atpick-up" class="dr-btn" onclick="atPickUp()" title="上一级">⬆ 上一级</button>
685
+ <span id="atpick-cur" style="flex:1;min-width:0;font-size:0.72rem;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left"></span>
686
+ </div>
687
+ <div id="atpick-list" style="max-height:200px;overflow-y:auto;scrollbar-width:thin;font-size:0.8rem"></div>
688
+ <div style="display:flex;gap:0.4rem;padding:0.45rem 0.5rem;border-top:1px solid var(--border)">
689
+ <button class="dr-btn" onclick="closeAtPicker()" style="flex:1;padding:0.45rem">取消</button>
690
+ <button onclick="addAtPicked()" style="flex:2;background:var(--blue2);color:#fff;border:none;border-radius:6px;padding:0.45rem;font-size:0.78rem;cursor:pointer">✓ 添加当前文件夹</button>
691
+ </div>
692
+ </div>
693
+ <details style="margin-top:0.35rem;font-size:0.72rem;color:var(--muted)">
694
+ <summary style="cursor:pointer">手动输入路径</summary>
695
+ <div style="display:flex;gap:0.4rem;margin-top:0.35rem">
696
+ <input id="atroot-new" class="slash-inp" placeholder="例如 D:\资料库" autocomplete="off" style="flex:1;font-size:0.82rem">
697
+ <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>
698
+ </div>
699
+ </details>
700
+ <div id="atroot-status" style="font-size:0.72rem;min-height:1.2em;margin-top:0.25rem;color:var(--muted)"></div>
701
+ </div>
651
702
  <div class="setting-group" data-stab="more" style="display:none">
652
703
  <h4>语音识别 (ASR)</h4>
653
704
  <div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.35rem">SenseVoice 或兼容服务的地址,留空表示禁用语音</div>
@@ -1188,7 +1239,7 @@
1188
1239
  // 快捷栏:skVisible 逐按钮勾选(唯一显隐依据,null=默认全显);skMode 仅记录最接近的预设(高亮用)
1189
1240
  const SK_LABELS = { draft:'📥 草稿', folder:'📂 文件夹', model:'模型', plan:'计划', compact:'压缩', resume:'恢复', clear:'清除', secrets:'🔑 密钥', guard:'🛡 护栏', followup:'🔄 跟进', git:'提交', memory:'记忆', skills:'技能', cost:'费用' };
1190
1241
  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 };
1242
+ const DEFAULT_SETTINGS = { parseOutput: true, showThinking: true, collapseTools: true, thinkCollapsed: true, rawClip: true, enterMode: 'auto', skMode: null, skVisible: null };
1192
1243
  let settings = loadSettings();
1193
1244
 
1194
1245
  function loadSettings() { try { return { ...DEFAULT_SETTINGS, ...JSON.parse(localStorage.getItem('myhi_chat_settings')) }; } catch { return { ...DEFAULT_SETTINGS }; } }
@@ -1283,6 +1334,93 @@
1283
1334
  updateEnterHint();
1284
1335
  syncSkModeUI();
1285
1336
 
1337
+ // ── @ 引用范围(项目外目录):客户自助配置,@ 补全可引用这些目录 ──
1338
+ let _atRoots = null;
1339
+ function renderAtRoots() {
1340
+ const el = document.getElementById('atroots-list');
1341
+ if (!el) return;
1342
+ el.innerHTML = (_atRoots || []).length
1343
+ ? _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('')
1344
+ : '<div style="font-size:0.72rem;color:var(--muted)">(未配置——@ 只搜任务目录)</div>';
1345
+ }
1346
+ function loadAtRoots() {
1347
+ return fetch('/api/atroots').then(r => r.json()).then(d => { _atRoots = d.roots || []; renderAtRoots(); }).catch(() => {});
1348
+ }
1349
+ async function saveAtRoots(roots) {
1350
+ const st = document.getElementById('atroot-status');
1351
+ try {
1352
+ const r = await fetch('/api/atroots', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ roots }) });
1353
+ const d = await r.json();
1354
+ if (!d.ok) { st.textContent = '保存失败:' + (d.error || ''); return false; }
1355
+ _atRoots = d.roots; renderAtRoots(); st.textContent = '已保存';
1356
+ return true;
1357
+ } catch (e) { st.textContent = '保存出错:' + e.message; return false; }
1358
+ }
1359
+ window.addAtRoot = function() {
1360
+ const inp = document.getElementById('atroot-new');
1361
+ const v = inp.value.trim();
1362
+ if (!v) return;
1363
+ saveAtRoots([...(_atRoots || []), v]).then(ok => { if (ok) inp.value = ''; });
1364
+ };
1365
+ window.delAtRoot = function(i) { saveAtRoots((_atRoots || []).filter((_, j) => j !== i)); };
1366
+
1367
+ // 文件夹点选(复用 /api/dirs 目录浏览):普通用户不会敲绝对路径,点进去选中即可
1368
+ let _atPick = { current: '', parent: null };
1369
+ window.openAtPicker = function() {
1370
+ document.getElementById('atpick').style.display = '';
1371
+ document.getElementById('atpick-open').style.display = 'none';
1372
+ atPickGo('');
1373
+ };
1374
+ window.closeAtPicker = function() {
1375
+ document.getElementById('atpick').style.display = 'none';
1376
+ document.getElementById('atpick-open').style.display = '';
1377
+ };
1378
+ window.atPickGo = async function(path) {
1379
+ const list = document.getElementById('atpick-list');
1380
+ list.innerHTML = '<div style="padding:0.6rem;color:var(--muted);font-size:0.75rem">读取中…</div>';
1381
+ try {
1382
+ const r = await fetch('/api/dirs' + (path ? '?path=' + encodeURIComponent(path) : ''));
1383
+ const d = await r.json();
1384
+ if (!r.ok) throw new Error(d.error || r.status);
1385
+ _atPick = d;
1386
+ const cur = document.getElementById('atpick-cur');
1387
+ cur.textContent = '\u200e' + d.current; // rtl 截断时保留路径尾部;LRM 防标点跑位
1388
+ cur.title = d.current;
1389
+ document.getElementById('atpick-up').disabled = !d.parent;
1390
+ document.getElementById('atpick-up').style.opacity = d.parent ? '' : '0.4';
1391
+ const dv = document.getElementById('atpick-drives');
1392
+ dv.style.display = (d.drives || []).length ? 'flex' : 'none';
1393
+ dv.innerHTML = '';
1394
+ (d.drives || []).forEach(dr => {
1395
+ const b = document.createElement('button');
1396
+ b.className = 'dr-btn';
1397
+ b.textContent = '💽 ' + dr.replace(/\\$/, '');
1398
+ if (d.current.toUpperCase().startsWith(dr.toUpperCase())) b.style.cssText = 'background:var(--blue2);color:#fff';
1399
+ b.onclick = () => atPickGo(dr);
1400
+ dv.appendChild(b);
1401
+ });
1402
+ list.innerHTML = '';
1403
+ if (!d.dirs.length) list.innerHTML = '<div style="padding:0.6rem;color:var(--muted);font-size:0.75rem">(没有子文件夹)</div>';
1404
+ d.dirs.forEach(x => {
1405
+ const row = document.createElement('div');
1406
+ row.className = 'atpick-row';
1407
+ row.textContent = '📁 ' + x.name;
1408
+ row.onclick = () => atPickGo(x.path);
1409
+ list.appendChild(row);
1410
+ });
1411
+ } catch (e) {
1412
+ list.innerHTML = '<div style="padding:0.6rem;color:var(--red);font-size:0.75rem">读取失败:' + escHtml(String(e.message || e)) +
1413
+ (String(e.message).includes('权限') ? '(仅管理员可浏览目录,可展开下方「手动输入路径」)' : '') + '</div>';
1414
+ }
1415
+ };
1416
+ window.atPickUp = function() { if (_atPick.parent) atPickGo(_atPick.parent); };
1417
+ window.addAtPicked = async function() {
1418
+ const p = _atPick.current;
1419
+ if (!p) return;
1420
+ if ((_atRoots || []).includes(p)) { document.getElementById('atroot-status').textContent = '这个文件夹已经在列表里了'; return; }
1421
+ if (await saveAtRoots([...(_atRoots || []), p])) closeAtPicker();
1422
+ };
1423
+
1286
1424
  // 输出累积
1287
1425
  let outputBuffer = '';
1288
1426
  let outputTimer = null;
@@ -2079,6 +2217,7 @@
2079
2217
  block.innerHTML = renderAnsiHtml(text);
2080
2218
  el.appendChild(block);
2081
2219
  parent.appendChild(el);
2220
+ rawClipSync(el);
2082
2221
  return;
2083
2222
  }
2084
2223
 
@@ -2259,6 +2398,7 @@
2259
2398
  // 限制单块行数
2260
2399
  const lines = block.innerHTML.split('\n');
2261
2400
  if (lines.length > 300) block.innerHTML = lines.slice(-200).join('\n');
2401
+ rawClipSync(last);
2262
2402
  scrollToBottom();
2263
2403
  return;
2264
2404
  }
@@ -2270,10 +2410,68 @@
2270
2410
  block.innerHTML = renderAnsiHtml(raw);
2271
2411
  msg.appendChild(block);
2272
2412
  chatArea.appendChild(msg);
2413
+ rawClipSync(msg);
2273
2414
  trimMessages();
2274
2415
  scrollToBottom();
2275
2416
  }
2276
2417
 
2418
+ // 复制到剪贴板。HTTP 访问(非安全上下文)没有 navigator.clipboard,退回 textarea + execCommand
2419
+ async function copyText(text) {
2420
+ try {
2421
+ if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); return true; }
2422
+ } catch {}
2423
+ const ta = document.createElement('textarea');
2424
+ ta.value = text;
2425
+ ta.setAttribute('readonly', '');
2426
+ ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0;pointer-events:none';
2427
+ document.body.appendChild(ta);
2428
+ ta.select(); ta.setSelectionRange(0, text.length); // iOS 需要 setSelectionRange
2429
+ let ok = false;
2430
+ try { ok = document.execCommand('copy'); } catch {}
2431
+ ta.remove();
2432
+ return ok;
2433
+ }
2434
+
2435
+ // 工具输出局部/全部显示。默认跟设置 rawClip;用户点过按钮的块记在 dataset.open,内容追加时保持。
2436
+ // 只有内容真的超出 8 行高度才出按钮;回放时元素可能还在 fragment 里没上屏,等上屏再量。
2437
+ function rawClipSync(msg, tries = 0) {
2438
+ if (!msg.isConnected) { if (tries < 30) requestAnimationFrame(() => rawClipSync(msg, tries + 1)); return; }
2439
+ const block = msg.querySelector('.code-block');
2440
+ if (!block) return;
2441
+ if (!msg.querySelector('.raw-copy')) {
2442
+ const cp = document.createElement('button');
2443
+ cp.className = 'raw-copy';
2444
+ cp.textContent = '复制';
2445
+ cp.title = '复制全部输出';
2446
+ cp.onclick = async () => {
2447
+ const ok = await copyText(block.textContent.replace(/\n+$/, ''));
2448
+ cp.textContent = ok ? '已复制' : '复制失败';
2449
+ cp.classList.add('done');
2450
+ clearTimeout(cp._t);
2451
+ cp._t = setTimeout(() => { cp.textContent = '复制'; cp.classList.remove('done'); }, 1500);
2452
+ };
2453
+ msg.appendChild(cp);
2454
+ }
2455
+ const clip = msg.dataset.open ? msg.dataset.open !== '1' : settings.rawClip !== false;
2456
+ msg.classList.add('clip'); // 先按局部高度量一次,判断是否超出
2457
+ const overflow = block.scrollHeight > block.clientHeight + 2;
2458
+ msg.classList.toggle('clip', clip && overflow);
2459
+ let btn = msg.querySelector('.raw-toggle');
2460
+ if (!overflow) { btn?.remove(); return; }
2461
+ if (!btn) {
2462
+ btn = document.createElement('button');
2463
+ btn.className = 'raw-toggle';
2464
+ btn.onclick = () => {
2465
+ msg.dataset.open = msg.classList.contains('clip') ? '1' : '0';
2466
+ rawClipSync(msg);
2467
+ if (msg.dataset.open === '0') msg.scrollIntoView({ block: 'nearest' }); // 收起后别让视口停在空白处
2468
+ };
2469
+ msg.appendChild(btn);
2470
+ }
2471
+ const n = block.textContent.replace(/\n+$/, '').split('\n').length;
2472
+ btn.textContent = msg.classList.contains('clip') ? `展开全部(共 ${n} 行)` : '收起';
2473
+ }
2474
+
2277
2475
  function addHistoryBlock(raw) {
2278
2476
  if (!stripAnsi(raw).trim()) return;
2279
2477
  let lines = raw.split('\n');
@@ -3152,7 +3350,7 @@
3152
3350
  const dir = cut >= 0 ? path.slice(0, cut + 1) : '';
3153
3351
  const name = cut >= 0 ? path.slice(cut + 1) : path;
3154
3352
  const ico = f.dir ? '📁' : (isEditableFile(name) ? '📝' : '📄');
3155
- const hint = f.dir ? '文件夹' : '引用';
3353
+ const hint = f.dir ? (f.abs ? '项目外目录' : '文件夹') : (f.abs ? '项目外' : '引用');
3156
3354
  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
3355
  }
3158
3356
 
@@ -3209,10 +3407,22 @@
3209
3407
  cmdInput.addEventListener('input', (e) => { if (!e.isComposing) mentionUpdate(); });
3210
3408
  cmdInput.addEventListener('keyup', (e) => { if (!['ArrowUp','ArrowDown','Escape','Enter'].includes(e.key)) mentionUpdate(); });
3211
3409
  cmdInput.addEventListener('blur', () => setTimeout(mentionClose, 120)); // 等候选中的 pointerdown 先落地
3410
+ // 触摸滚动 vs 点击区分:pointerdown 只防 textarea 失焦并记起点,
3411
+ // pointerup 时位移 <8px 才选中——否则手机上滚清单的手势一起就误引用(1.3.176 用户反馈)
3412
+ let _mentionDown = null;
3212
3413
  _mentionBox.addEventListener('pointerdown', (e) => {
3213
3414
  const row = e.target.closest('.mi[data-i]');
3214
- if (row) { e.preventDefault(); mentionPick(_mention.items[Number(row.dataset.i)]); }
3415
+ if (row) e.preventDefault();
3416
+ _mentionDown = row ? { x: e.clientX, y: e.clientY, row } : null;
3417
+ });
3418
+ _mentionBox.addEventListener('pointerup', (e) => {
3419
+ const d = _mentionDown; _mentionDown = null;
3420
+ if (!d) return;
3421
+ if (Math.abs(e.clientX - d.x) >= 8 || Math.abs(e.clientY - d.y) >= 8) return; // 滚动手势
3422
+ const row = e.pointerType === 'touch' ? d.row : e.target.closest('.mi[data-i]');
3423
+ if (row) mentionPick(_mention.items[Number(row.dataset.i)]);
3215
3424
  });
3425
+ _mentionBox.addEventListener('pointercancel', () => { _mentionDown = null; });
3216
3426
  window.addEventListener('resize', () => { if (_mention.open) mentionClose(); });
3217
3427
 
3218
3428
  cmdInput.addEventListener('keydown', (e) => {
@@ -3289,7 +3499,34 @@
3289
3499
  }
3290
3500
 
3291
3501
  // ── 设置面板 ──────────────────────────────────
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'); };
3502
+ // ── 会话内使用说明(精简版;完整版在首页 ❓ 帮助)──────────────
3503
+ window.openChatHelp = function() {
3504
+ overlay.innerHTML = '';
3505
+ overlay.classList.remove('hidden');
3506
+ const wrap = document.createElement('div');
3507
+ wrap.style.cssText = 'position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:1rem;background:rgba(0,0,0,0.55)';
3508
+ const box = document.createElement('div');
3509
+ 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';
3510
+ box.innerHTML = `
3511
+ <div style="font-size:1rem;font-weight:700;margin-bottom:0.5rem">💬 会话怎么用</div>
3512
+ <b>发消息</b>:像聊天一样把需求说清楚即可;触屏回车=换行,用发送键发送。<br>
3513
+ <b>让它读文件</b>:打 <b>@</b> 弹文件搜索(含设置里配的项目外目录),选中插路径;或点「文件」面板的 <b>@</b> 引用。<br>
3514
+ <b>工具/思考</b>:AI 干活的过程默认折叠,点「🔧 N 个工具调用」展开看命令与输出。<br>
3515
+ <b>AI 忙时</b>:输入框变「存草稿」,先记着干完取回;勾自动接力会自动发出。<br>
3516
+ <b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰ 可列出你发过的消息点击跳转。<br>
3517
+ <b>常用按钮</b>:模型/计划/压缩/恢复/清除/密钥/护栏/提交——普通用户可在设置→快捷栏精简。<br>
3518
+ <span style="color:var(--muted)">更多说明见首页顶栏 ❓ 帮助。</span>`;
3519
+ const closeBtn = document.createElement('button');
3520
+ closeBtn.textContent = '知道了';
3521
+ 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';
3522
+ closeBtn.onclick = () => overlay.classList.add('hidden');
3523
+ box.appendChild(closeBtn);
3524
+ wrap.appendChild(box);
3525
+ wrap.onclick = (e) => { if (e.target === wrap) overlay.classList.add('hidden'); };
3526
+ overlay.appendChild(wrap);
3527
+ };
3528
+
3529
+ 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
3530
  // 设置抽屉分类页签(1.3.174:一屏只看一类,不再长条堆叠)
3294
3531
  function applySettingsTab(tab) {
3295
3532
  document.querySelectorAll('#settings-tabs .stab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
@@ -3828,17 +4065,49 @@
3828
4065
  document.getElementById('set-thinking').checked = settings.showThinking;
3829
4066
  document.getElementById('set-collapse').checked = settings.collapseTools;
3830
4067
  document.getElementById('set-thinkcollapse').checked = settings.thinkCollapsed !== false;
4068
+ document.getElementById('set-rawclip').checked = settings.rawClip !== false;
3831
4069
  syncEnterModeUI();
3832
4070
  syncSkModeUI();
3833
4071
  // 从服务端加载 ASR URL
3834
4072
  fetch('/api/server-config').then(r => r.json()).then(d => {
3835
4073
  document.getElementById('set-asr-url').value = d.asrUrl || '';
3836
4074
  }).catch(() => {});
4075
+ fetch('/api/machine-name').then(r => r.json()).then(d => {
4076
+ const inp = document.getElementById('set-machine-name');
4077
+ inp.placeholder = d.hostname || '';
4078
+ inp.value = d.name === d.hostname ? '' : (d.name || '');
4079
+ inp.disabled = !!d.fromEnv;
4080
+ document.getElementById('set-machine-status').textContent = d.fromEnv ? '已由环境变量 MYHI_NAME 指定,此处不可改' : '';
4081
+ }).catch(() => {});
3837
4082
  }
3838
4083
  document.getElementById('set-parse').addEventListener('change', (e) => { settings.parseOutput = e.target.checked; saveSettings(); });
3839
4084
  document.getElementById('set-thinking').addEventListener('change', (e) => { settings.showThinking = e.target.checked; saveSettings(); });
3840
4085
  document.getElementById('set-collapse').addEventListener('change', (e) => { settings.collapseTools = e.target.checked; saveSettings(); });
3841
4086
  document.getElementById('set-thinkcollapse').addEventListener('change', (e) => { settings.thinkCollapsed = e.target.checked; saveSettings(); });
4087
+ document.getElementById('set-rawclip').addEventListener('change', (e) => {
4088
+ settings.rawClip = e.target.checked; saveSettings();
4089
+ // 已渲染的输出块立即跟随新默认(用户手动点过的块也一并重置)
4090
+ chatArea.querySelectorAll('.msg-raw').forEach(m => { delete m.dataset.open; rawClipSync(m); });
4091
+ });
4092
+
4093
+ window.saveMachineName = async function() {
4094
+ const statusEl = document.getElementById('set-machine-status');
4095
+ try {
4096
+ const resp = await fetch('/api/machine-name', {
4097
+ method: 'POST',
4098
+ headers: { 'Content-Type': 'application/json' },
4099
+ body: JSON.stringify({ name: document.getElementById('set-machine-name').value.trim() }),
4100
+ });
4101
+ const data = await resp.json();
4102
+ if (!data.ok) throw new Error(data.error || '未知错误');
4103
+ window.myhiSetMachineName?.(data.name);
4104
+ statusEl.style.color = 'var(--green)';
4105
+ statusEl.textContent = `已保存:${data.name}(其他已打开的页面刷新后生效)`;
4106
+ } catch (e) {
4107
+ statusEl.style.color = 'var(--red)';
4108
+ statusEl.textContent = '保存失败: ' + e.message;
4109
+ }
4110
+ };
3842
4111
 
3843
4112
  window.saveAsrUrl = async function() {
3844
4113
  const url = document.getElementById('set-asr-url').value.trim();