@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/admin.html +498 -497
- package/dist/chat.html +273 -4
- package/dist/grid.html +448 -447
- package/dist/index.html +88 -2
- package/dist/index.js +4 -4
- package/dist/index.min.js +138 -138
- package/dist/lib/machine.js +50 -0
- package/dist/terminal.html +1064 -1063
- package/package.json +1 -1
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)
|
|
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
|
-
|
|
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();
|