@wendongfly/myhi 1.3.181 → 1.3.184

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
@@ -22,6 +22,7 @@
22
22
  }catch(e){}})();
23
23
  </script>
24
24
  <style>
25
+ body.no-desktop [onclick="fpOpenFolder()"] { display: none !important; }
25
26
  /* ── 主题 & 字号(默认深色,值=原配色,深色零变化)───────── */
26
27
  :root {
27
28
  --bg:#0d1117; --surface:#161b22; --surface2:#21262d; --surface3:#1c2128; --border:#30363d;
@@ -267,6 +268,8 @@
267
268
  #mention-box .mi .m-name { color: var(--text); white-space: nowrap; }
268
269
  #mention-box .mi .m-hint { color: var(--muted); font-size: 0.66rem; margin-left: auto; padding-left: 0.5rem; flex-shrink: 0; }
269
270
  #mention-box .mi-empty { padding: 0.55rem 0.7rem; color: var(--muted); font-size: 0.75rem; }
271
+ #mention-box .mi-head { padding: 0.4rem 0.6rem 0.15rem; color: var(--muted); font-size: 0.66rem; font-weight: 600; letter-spacing: 0.04em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
272
+ #mention-box .mi-tip { padding: 0.35rem 0.6rem 0.2rem; color: var(--dim, var(--muted)); font-size: 0.64rem; border-top: 1px solid var(--border); margin-top: 0.2rem; }
270
273
  #cmd-input { display: block; width: 100%; background: transparent; color: var(--text); border: none; padding: 0.6rem 0.75rem 0.3rem; font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; font-size: 0.85rem; outline: none; resize: none; line-height: 1.5; max-height: 140px; overflow-y: auto; }
271
274
  #cmd-input::placeholder { color: var(--dimmer); }
272
275
  @media (pointer: coarse) { #cmd-input { font-size: 1rem; padding: 0.75rem 0.75rem 0.35rem; line-height: 1.55; } }
@@ -410,8 +413,6 @@
410
413
  .dr-text { color: var(--text); font-size: 0.82rem; line-height: 1.5; word-break: break-word; white-space: pre-wrap; }
411
414
  .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); }
412
415
  .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); }
415
416
  .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; }
416
417
  .dr-btn.take { background: var(--green); color: #fff; font-weight: 600; }
417
418
  .dr-btn:active { opacity: 0.75; }
@@ -665,48 +666,9 @@
665
666
  <div id="sk-visible-list" style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0.15rem 0.6rem;margin-top:0.5rem"></div>
666
667
  </div>
667
668
  <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>
702
- <div class="setting-group" data-stab="more" style="display:none">
703
- <h4>语音识别 (ASR)</h4>
704
- <div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.35rem">SenseVoice 或兼容服务的地址,留空表示禁用语音</div>
705
- <div style="display:flex;gap:0.4rem">
706
- <input id="set-asr-url" class="slash-inp" placeholder="http://192.168.x.x:8765" autocomplete="off" style="flex:1;font-size:0.82rem">
707
- <button onclick="saveAsrUrl()" 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>
708
- </div>
709
- <div id="set-asr-status" style="font-size:0.72rem;min-height:1.2em;margin-top:0.25rem;color:var(--muted)"></div>
669
+ <h4>全局设置</h4>
670
+ <div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.45rem;line-height:1.5">本机名称、@ 引用范围、语音识别、账户等对所有任务生效的设置,统一放在首页的 ⚙ 设置里。这里的「外观 / 聊天 / 快捷栏」只影响当前这台设备。</div>
671
+ <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>
710
672
  </div>
711
673
  <div class="setting-group" data-stab="more" style="display:none">
712
674
  <h4>服务管理</h4>
@@ -1145,6 +1107,8 @@
1145
1107
  ansi.use_classes = false;
1146
1108
 
1147
1109
  fetch('/api/me').then(r => r.json()).then(data => {
1110
+ // 服务端没有桌面(headless 部署):「打开当前文件夹」必然无效,直接藏掉,只留文件面板
1111
+ if (data?.desktop === false) document.body.classList.add('no-desktop');
1148
1112
  // 打包默认快捷栏预设:用户没自定义过勾选(skVisible 为 null)时采用服务端 config.json 的 skMode
1149
1113
  if (data?.skMode && settings.skVisible == null) {
1150
1114
  settings.skVisible = data.skMode === 'normal' ? skPresetNormal() : skPresetAll();
@@ -1334,93 +1298,6 @@
1334
1298
  updateEnterHint();
1335
1299
  syncSkModeUI();
1336
1300
 
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 readApiJson(r);
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
-
1424
1301
  // 输出累积
1425
1302
  let outputBuffer = '';
1426
1303
  let outputTimer = null;
@@ -3330,7 +3207,7 @@
3330
3207
  // 输入框里打出 @词(@ 后无空白)时浮出文件搜索层:递归搜会话目录、
3331
3208
  // 最近修改优先;选中把 @词 替换成真实相对路径。「让 AI 读某个文件」
3332
3209
  // 不再需要手打文件名。也可在 📄 文件面板点「引用」达到同样效果。
3333
- const _mention = { open: false, start: -1, items: [], idx: 0 };
3210
+ const _mention = { open: false, start: -1, items: [], idx: 0, browse: null, seq: 0 };
3334
3211
  const _mentionBox = document.createElement('div');
3335
3212
  _mentionBox.id = 'mention-box';
3336
3213
  document.body.appendChild(_mentionBox);
@@ -3349,24 +3226,38 @@
3349
3226
 
3350
3227
  function mentionClose() {
3351
3228
  _mention.open = false;
3229
+ _mention.browse = null;
3352
3230
  _mention.items = [];
3353
3231
  _mentionBox.style.display = 'none';
3354
3232
  }
3355
3233
 
3234
+ // 两种模式:只打 @ = 分层浏览(_mention.browse 记当前层,点文件夹进入下一层);
3235
+ // @ 后打了字 = 全局递归搜索(服务端已排好:本项目在前、项目外在后,匹配度优先)
3236
+ const MENTION_GROUP = { own: '本项目', ext: '项目外' };
3356
3237
  function mentionRowHTML(f, i) {
3357
- const path = f.name;
3358
- const cut = path.lastIndexOf('/');
3238
+ const on = i === _mention.idx ? 'on' : '';
3239
+ if (f.act === 'up') return `<div class="mi ${on}" data-i="${i}"><span class="m-ico">⬆</span><span class="m-name">上一级</span></div>`;
3240
+ if (f.act === 'self') return `<div class="mi ${on}" data-i="${i}"><span class="m-ico">✓</span><span class="m-name">引用这个文件夹</span><span class="m-hint">${f.abs ? '项目外目录' : '文件夹'}</span></div>`;
3241
+ const path = f.label || f.name;
3242
+ const cut = f.label ? -1 : path.lastIndexOf('/');
3359
3243
  const dir = cut >= 0 ? path.slice(0, cut + 1) : '';
3360
3244
  const name = cut >= 0 ? path.slice(cut + 1) : path;
3361
3245
  const ico = f.dir ? '📁' : (isEditableFile(name) ? '📝' : '📄');
3362
- const hint = f.dir ? (f.abs ? '项目外目录' : '文件夹') : (f.abs ? '项目外' : '引用');
3363
- 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>`;
3246
+ const hint = f.nav ? '进入 ›' : f.dir ? (f.abs ? '项目外目录' : '文件夹') : (f.abs ? '项目外' : '引用');
3247
+ return `<div class="mi ${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>`;
3364
3248
  }
3365
3249
 
3366
3250
  function mentionRender() {
3367
- _mentionBox.innerHTML = _mention.items.length
3368
- ? _mention.items.map((f, i) => mentionRowHTML(f, i)).join('')
3369
- : '<div class="mi-empty">没有匹配的文件</div>';
3251
+ const b = _mention.browse;
3252
+ let html = b && b.crumb ? `<div class="mi-head">📂 ${escHtml(b.crumb)}</div>` : '';
3253
+ let group = null;
3254
+ _mention.items.forEach((f, i) => {
3255
+ if (f.group && f.group !== group && !(b && b.crumb)) { group = f.group; html += `<div class="mi-head">${MENTION_GROUP[group]}</div>`; }
3256
+ html += mentionRowHTML(f, i);
3257
+ });
3258
+ if (!_mention.items.some(f => !f.act)) html += `<div class="mi-empty">${b ? '这个文件夹是空的' : '没有匹配的文件'}</div>`;
3259
+ if (b && _mention.items.some(f => f.nav)) html += '<div class="mi-tip">回车/点击进入文件夹 · Tab 直接引用文件夹 · 打字可全局搜索</div>';
3260
+ _mentionBox.innerHTML = html;
3370
3261
  const r = cmdInput.getBoundingClientRect();
3371
3262
  _mentionBox.style.left = Math.max(8, r.left) + 'px';
3372
3263
  _mentionBox.style.width = Math.max(r.width, 240) + 'px';
@@ -3382,20 +3273,68 @@
3382
3273
  try {
3383
3274
  const r = await fetch(`/api/files/${SESSION_ID}/search?q=${encodeURIComponent(token)}`);
3384
3275
  const d = await r.json();
3385
- if (!_mention.open || !d.ok) return;
3386
- _mention.items = d.files || [];
3276
+ if (!_mention.open || _mention.browse || !d.ok) return;
3277
+ _mention.items = (d.files || []).map(f => ({ ...f, group: f.abs ? 'ext' : 'own' }));
3387
3278
  _mention.idx = 0;
3388
3279
  mentionRender();
3389
3280
  } catch { mentionClose(); }
3390
3281
  }, 140);
3391
3282
  }
3392
3283
 
3284
+ // 分层浏览:root=null 顶层(本项目第一层 + 项目外根目录),-1 本项目内,i 第 i 个项目外根目录内
3285
+ async function mentionBrowse(root, path) {
3286
+ const seq = ++_mention.seq;
3287
+ try {
3288
+ const qs = root === null ? '' : `?root=${root}&path=${encodeURIComponent(path)}`;
3289
+ const r = await fetch(`/api/files/${SESSION_ID}/at-browse${qs}`);
3290
+ const d = await r.json();
3291
+ if (seq !== _mention.seq || !_mention.open || !d.ok) return;
3292
+ const items = [];
3293
+ let crumb = '';
3294
+ if (root !== null) {
3295
+ const rootFull = path ? d.self.slice(0, -(path.length + 1)) : d.self;
3296
+ const rootLabel = root === -1 ? '本项目' : '项目外 · ' + (rootFull.split('/').pop() || rootFull);
3297
+ crumb = rootLabel + (path ? ' / ' + path.split('/').join(' / ') : '');
3298
+ items.push({ act: 'up' });
3299
+ if (path || root !== -1) items.push({ act: 'self', name: d.self, dir: true, abs: root !== -1 });
3300
+ }
3301
+ d.entries.forEach(e => items.push({ ...e, root: root === null ? -1 : root, nav: e.dir, group: root === null ? 'own' : null }));
3302
+ (d.roots || []).forEach(x => items.push({ name: x.name, dir: true, abs: true, root: x.i, rel: '', nav: true, group: 'ext' }));
3303
+ _mention.browse = { root, path, crumb };
3304
+ _mention.items = items;
3305
+ _mention.idx = Math.max(0, items.findIndex(f => !f.act)); // 默认落在第一个条目上,不是「上一级」
3306
+ mentionRender();
3307
+ } catch { mentionClose(); }
3308
+ }
3309
+
3393
3310
  function mentionUpdate() {
3394
3311
  const t = mentionToken();
3395
3312
  if (!t) return mentionClose();
3313
+ const token = cmdInput.value.slice(t.start + 1, cmdInput.selectionStart ?? cmdInput.value.length);
3314
+ const wasOpen = _mention.open && _mention.start === t.start;
3396
3315
  _mention.open = true;
3397
3316
  _mention.start = t.start;
3398
- mentionSearch(cmdInput.value.slice(t.start + 1, cmdInput.selectionStart ?? cmdInput.value.length));
3317
+ if (!token) {
3318
+ // 已在浏览某一层就别重置(keyup 等事件会反复进来);新打开或从搜索删回空才回顶层
3319
+ if (!(wasOpen && _mention.browse)) { _mention.browse = { root: null, path: '', crumb: '' }; mentionBrowse(null, ''); }
3320
+ return;
3321
+ }
3322
+ _mention.browse = null;
3323
+ _mention.seq++;
3324
+ mentionSearch(token);
3325
+ }
3326
+
3327
+ // 回车/点击:文件夹(浏览模式)进入、上一级返回、其余引用;Tab = 文件夹也直接引用
3328
+ function mentionActivate(f, pickDir) {
3329
+ if (!f) return;
3330
+ if (f.act === 'up') {
3331
+ const b = _mention.browse;
3332
+ if (b.root !== null && b.path) mentionBrowse(b.root, b.path.includes('/') ? b.path.slice(0, b.path.lastIndexOf('/')) : '');
3333
+ else mentionBrowse(null, '');
3334
+ return;
3335
+ }
3336
+ if (f.nav && !pickDir) return mentionBrowse(f.root, f.rel);
3337
+ mentionPick(f);
3399
3338
  }
3400
3339
 
3401
3340
  function mentionPick(f) {
@@ -3429,7 +3368,7 @@
3429
3368
  if (!d) return;
3430
3369
  if (Math.abs(e.clientX - d.x) >= 8 || Math.abs(e.clientY - d.y) >= 8) return; // 滚动手势
3431
3370
  const row = e.pointerType === 'touch' ? d.row : e.target.closest('.mi[data-i]');
3432
- if (row) mentionPick(_mention.items[Number(row.dataset.i)]);
3371
+ if (row) mentionActivate(_mention.items[Number(row.dataset.i)]);
3433
3372
  });
3434
3373
  _mentionBox.addEventListener('pointercancel', () => { _mentionDown = null; });
3435
3374
  window.addEventListener('resize', () => { if (_mention.open) mentionClose(); });
@@ -3443,7 +3382,7 @@
3443
3382
  e.preventDefault(); _mention.idx = (_mention.idx - 1 + _mention.items.length) % _mention.items.length; mentionRender(); return;
3444
3383
  }
3445
3384
  if ((e.key === 'Enter' || e.key === 'Tab') && _mention.items.length && !e.isComposing && e.keyCode !== 229) {
3446
- e.preventDefault(); mentionPick(_mention.items[_mention.idx]); return; // 补全层开着时回车=选中,不是发送
3385
+ e.preventDefault(); mentionActivate(_mention.items[_mention.idx], e.key === 'Tab'); return; // 补全层开着时回车=选中/进入,不是发送
3447
3386
  }
3448
3387
  if (e.key === 'Escape') { e.preventDefault(); mentionClose(); return; } // 先关补全,别误触中断 AI
3449
3388
  }
@@ -3535,7 +3474,7 @@
3535
3474
  overlay.appendChild(wrap);
3536
3475
  };
3537
3476
 
3538
- 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'); };
3477
+ 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'); };
3539
3478
  // 设置抽屉分类页签(1.3.174:一屏只看一类,不再长条堆叠)
3540
3479
  function applySettingsTab(tab) {
3541
3480
  document.querySelectorAll('#settings-tabs .stab').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
@@ -3562,7 +3501,7 @@
3562
3501
  const d = await r.json();
3563
3502
  if (d.ok) {
3564
3503
  closeFilePanel();
3565
- addStatusMessage('📂 已在本机打开文件夹 ' + (d.cwd || ''));
3504
+ addStatusMessage((d.via === 'task' ? '📂 已在本机桌面打开文件夹(myhi 在后台会话运行,经计划任务打开)' : '📂 已在本机打开文件夹 ') + (d.cwd || ''));
3566
3505
  } else {
3567
3506
  addStatusMessage('打开失败: ' + (d.error || '') + '(若你是远程访问本面板,此按钮本就不生效)');
3568
3507
  }
@@ -4077,17 +4016,6 @@
4077
4016
  document.getElementById('set-rawclip').checked = settings.rawClip !== false;
4078
4017
  syncEnterModeUI();
4079
4018
  syncSkModeUI();
4080
- // 从服务端加载 ASR URL
4081
- fetch('/api/server-config').then(r => r.json()).then(d => {
4082
- document.getElementById('set-asr-url').value = d.asrUrl || '';
4083
- }).catch(() => {});
4084
- fetch('/api/machine-name').then(r => r.json()).then(d => {
4085
- const inp = document.getElementById('set-machine-name');
4086
- inp.placeholder = d.hostname || '';
4087
- inp.value = d.name === d.hostname ? '' : (d.name || '');
4088
- inp.disabled = !!d.fromEnv;
4089
- document.getElementById('set-machine-status').textContent = d.fromEnv ? '已由环境变量 MYHI_NAME 指定,此处不可改' : '';
4090
- }).catch(() => {});
4091
4019
  }
4092
4020
  document.getElementById('set-parse').addEventListener('change', (e) => { settings.parseOutput = e.target.checked; saveSettings(); });
4093
4021
  document.getElementById('set-thinking').addEventListener('change', (e) => { settings.showThinking = e.target.checked; saveSettings(); });
@@ -4099,50 +4027,6 @@
4099
4027
  chatArea.querySelectorAll('.msg-raw').forEach(m => { delete m.dataset.open; rawClipSync(m); });
4100
4028
  });
4101
4029
 
4102
- window.saveMachineName = async function() {
4103
- const statusEl = document.getElementById('set-machine-status');
4104
- try {
4105
- const resp = await fetch('/api/machine-name', {
4106
- method: 'POST',
4107
- headers: { 'Content-Type': 'application/json' },
4108
- body: JSON.stringify({ name: document.getElementById('set-machine-name').value.trim() }),
4109
- });
4110
- const data = await readApiJson(resp);
4111
- if (!data.ok) throw new Error(data.error || '未知错误');
4112
- window.myhiSetMachineName?.(data.name);
4113
- statusEl.style.color = 'var(--green)';
4114
- statusEl.textContent = `已保存:${data.name}(其他已打开的页面刷新后生效)`;
4115
- } catch (e) {
4116
- statusEl.style.color = 'var(--red)';
4117
- statusEl.textContent = '保存失败: ' + e.message;
4118
- }
4119
- };
4120
-
4121
- window.saveAsrUrl = async function() {
4122
- const url = document.getElementById('set-asr-url').value.trim();
4123
- const statusEl = document.getElementById('set-asr-status');
4124
- statusEl.style.color = 'var(--muted)';
4125
- statusEl.textContent = '保存中...';
4126
- try {
4127
- const resp = await fetch('/api/server-config', {
4128
- method: 'POST',
4129
- headers: { 'Content-Type': 'application/json' },
4130
- body: JSON.stringify({ asrUrl: url }),
4131
- });
4132
- const data = await resp.json();
4133
- if (data.ok) {
4134
- statusEl.style.color = 'var(--green)';
4135
- statusEl.textContent = url ? '已保存,重启后生效(如已设置 ASR_URL 环境变量则以环境变量为准)' : '已清空';
4136
- } else {
4137
- statusEl.style.color = 'var(--red)';
4138
- statusEl.textContent = '保存失败: ' + (data.error || '未知错误');
4139
- }
4140
- } catch (e) {
4141
- statusEl.style.color = 'var(--red)';
4142
- statusEl.textContent = '请求失败: ' + e.message;
4143
- }
4144
- };
4145
-
4146
4030
  window.restartMyhi = async function() {
4147
4031
  if (!confirm('确认重启 myhi 服务?\n重启期间连接会短暂中断,页面将自动重连。')) return;
4148
4032
  try {