@wendongfly/myhi 1.3.182 → 1.3.185
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/myhi.js +873 -873
- package/dist/chat.html +197 -34
- package/dist/index.html +102 -162
- package/dist/index.js +10 -10
- package/dist/index.min.js +123 -121
- package/package.json +1 -1
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;
|
|
@@ -156,7 +157,7 @@
|
|
|
156
157
|
.sub-act .k { color: var(--muted); }
|
|
157
158
|
.sub-summary { background: var(--surface); border: 1px solid var(--surface2); border-radius: 6px; padding: 0.4rem 0.5rem; margin-top: 0.35rem; font-size: 0.75rem; white-space: pre-wrap; word-break: break-word; max-height: 260px; overflow-y: auto; }
|
|
158
159
|
|
|
159
|
-
/*
|
|
160
|
+
/* 往上翻看历史(含 ☰ 定位跳转后)→ 底部浮动「↓ 回到底部」,期间来了新消息变「↓ 新消息」;点击回到底部 */
|
|
160
161
|
#new-msg-btn { position: fixed; left: 50%; transform: translateX(-50%); bottom: 92px; z-index: 60; background: var(--blue2, #4a9eff); color: #fff; border: none; border-radius: 16px; padding: 0.35rem 0.95rem; font-size: 0.78rem; font-weight: 600; cursor: pointer; box-shadow: 0 3px 10px rgba(0,0,0,0.35); display: none; }
|
|
161
162
|
#new-msg-btn.show { display: block; }
|
|
162
163
|
#new-msg-btn:active { opacity: 0.8; }
|
|
@@ -197,10 +198,16 @@
|
|
|
197
198
|
.raw-toggle:hover { color: var(--text); border-color: var(--blue); }
|
|
198
199
|
/* 工具输出复制:右上角,桌面端悬停才现身,触屏常显 */
|
|
199
200
|
.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
|
+
.raw-copy, .cp-btn { 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
202
|
.msg-raw:hover .raw-copy, .raw-copy.done { opacity: 1; }
|
|
202
|
-
.raw-copy:hover { color: var(--text); border-color: var(--blue); }
|
|
203
|
+
.raw-copy:hover, .cp-btn:hover { color: var(--text); border-color: var(--blue); }
|
|
203
204
|
@media (hover: none) { .raw-copy { opacity: 0.85; } }
|
|
205
|
+
/* 消息里其他带框内容(代码块/表格/diff/工具详情…)同款复制按钮,见 attachCopyBtns */
|
|
206
|
+
.cpw, .cp-host { position: relative; }
|
|
207
|
+
.cp-btn { user-select: none; }
|
|
208
|
+
.cpw:hover > .cp-btn, .cp-host:hover > .cp-btn, .cp-btn.done { opacity: 1; }
|
|
209
|
+
.tool-body:not(.open) + .cp-btn { display: none; }
|
|
210
|
+
@media (hover: none) { .cp-btn { opacity: 0.85; } }
|
|
204
211
|
|
|
205
212
|
/* 历史和状态 */
|
|
206
213
|
.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; }
|
|
@@ -267,6 +274,8 @@
|
|
|
267
274
|
#mention-box .mi .m-name { color: var(--text); white-space: nowrap; }
|
|
268
275
|
#mention-box .mi .m-hint { color: var(--muted); font-size: 0.66rem; margin-left: auto; padding-left: 0.5rem; flex-shrink: 0; }
|
|
269
276
|
#mention-box .mi-empty { padding: 0.55rem 0.7rem; color: var(--muted); font-size: 0.75rem; }
|
|
277
|
+
#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; }
|
|
278
|
+
#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
279
|
#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
280
|
#cmd-input::placeholder { color: var(--dimmer); }
|
|
272
281
|
@media (pointer: coarse) { #cmd-input { font-size: 1rem; padding: 0.75rem 0.75rem 0.35rem; line-height: 1.55; } }
|
|
@@ -664,7 +673,7 @@
|
|
|
664
673
|
</div>
|
|
665
674
|
<div class="setting-group" data-stab="more" style="display:none">
|
|
666
675
|
<h4>全局设置</h4>
|
|
667
|
-
<div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.45rem;line-height:1.5"
|
|
676
|
+
<div style="font-size:0.72rem;color:var(--muted);margin-bottom:0.45rem;line-height:1.5">本机名称、语音识别、账户等对所有任务生效的设置,统一放在首页的 ⚙ 设置里。这里的「外观 / 聊天 / 快捷栏」只影响当前这台设备。</div>
|
|
668
677
|
<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>
|
|
669
678
|
</div>
|
|
670
679
|
<div class="setting-group" data-stab="more" style="display:none">
|
|
@@ -682,8 +691,8 @@
|
|
|
682
691
|
</div>
|
|
683
692
|
</div>
|
|
684
693
|
|
|
685
|
-
<!--
|
|
686
|
-
<button id="new-msg-btn" onclick="jumpToBottom()">↓
|
|
694
|
+
<!-- 往上翻历史时的浮动按钮(来了新消息时提示「新消息」),点击回到底部 -->
|
|
695
|
+
<button id="new-msg-btn" onclick="jumpToBottom()">↓ 回到底部</button>
|
|
687
696
|
|
|
688
697
|
<!-- 模型选择面板 -->
|
|
689
698
|
<!-- 会话数据面板:上下文水位 + 本会话成本/token 明细(点顶栏水位 chip 打开)-->
|
|
@@ -1103,7 +1112,11 @@
|
|
|
1103
1112
|
const ansi = new AnsiUp();
|
|
1104
1113
|
ansi.use_classes = false;
|
|
1105
1114
|
|
|
1115
|
+
let _myRole = 'admin'; // 成员(operator)@ 只能浏览本项目/绑定目录,不提示项目外用法
|
|
1106
1116
|
fetch('/api/me').then(r => r.json()).then(data => {
|
|
1117
|
+
// 服务端没有桌面(headless 部署):「打开当前文件夹」必然无效,直接藏掉,只留文件面板
|
|
1118
|
+
if (data?.desktop === false) document.body.classList.add('no-desktop');
|
|
1119
|
+
_myRole = data?.role || 'admin';
|
|
1107
1120
|
// 打包默认快捷栏预设:用户没自定义过勾选(skVisible 为 null)时采用服务端 config.json 的 skMode
|
|
1108
1121
|
if (data?.skMode && settings.skVisible == null) {
|
|
1109
1122
|
settings.skVisible = data.skMode === 'normal' ? skPresetNormal() : skPresetAll();
|
|
@@ -2313,6 +2326,51 @@
|
|
|
2313
2326
|
return ok;
|
|
2314
2327
|
}
|
|
2315
2328
|
|
|
2329
|
+
function copyFlash(btn, ok) {
|
|
2330
|
+
btn.textContent = ok ? '已复制' : '复制失败';
|
|
2331
|
+
btn.classList.add('done');
|
|
2332
|
+
clearTimeout(btn._t);
|
|
2333
|
+
btn._t = setTimeout(() => { btn.textContent = '复制'; btn.classList.remove('done'); }, 1500);
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
// 消息里所有「带框」内容右上角都给复制按钮:代码块、表格(复制成制表符分隔,可直接粘进 Excel)、
|
|
2337
|
+
// diff、工具调用详情、子代理汇总、历史块、压缩摘要。工具原始输出(.msg-raw)有自己的按钮。
|
|
2338
|
+
// 渲染点很多(流式每个 chunk 都重写 innerHTML),统一用 MutationObserver 补挂,不逐处改。
|
|
2339
|
+
const COPY_SEL = '.msg-assistant .content pre, .md-table, .msg-diff, .tool-body, .sub-summary, .msg-synthetic .syn-body, .msg-history .code-block';
|
|
2340
|
+
function copyableText(el) {
|
|
2341
|
+
if (el.tagName === 'TABLE') return [...el.rows].map(r => [...r.cells].map(c => c.innerText.trim()).join('\t')).join('\n');
|
|
2342
|
+
const nodes = [...el.childNodes].filter(n => !n.classList?.contains('cp-btn'));
|
|
2343
|
+
// diff 每行是一个 <div>,按行拼;其余(pre / pre-wrap 文本)textContent 自带换行
|
|
2344
|
+
if (el.classList.contains('msg-diff')) return nodes.map(n => n.textContent).join('\n');
|
|
2345
|
+
return nodes.map(n => n.textContent).join('').replace(/\n+$/, '');
|
|
2346
|
+
}
|
|
2347
|
+
function attachCopyBtns() {
|
|
2348
|
+
chatArea.querySelectorAll(COPY_SEL).forEach(el => {
|
|
2349
|
+
if (el._cpBtn?.isConnected || el.closest('.msg-raw')) return;
|
|
2350
|
+
const btn = document.createElement('button');
|
|
2351
|
+
btn.className = 'cp-btn';
|
|
2352
|
+
btn.textContent = '复制';
|
|
2353
|
+
btn.title = el.tagName === 'TABLE' ? '复制表格(可直接粘贴到 Excel)' : '复制';
|
|
2354
|
+
btn.onclick = async (e) => { e.stopPropagation(); copyFlash(btn, await copyText(copyableText(el))); };
|
|
2355
|
+
if (el.classList.contains('msg')) {
|
|
2356
|
+
// 本身就是一条 .msg(diff):不能套壳,否则 trimMessages / lastElementChild 认不出
|
|
2357
|
+
el.classList.add('cp-host'); el.appendChild(btn);
|
|
2358
|
+
} else {
|
|
2359
|
+
// 套一层壳挂按钮:代码块/工具详情自己会横竖滚动,按钮放里面会跟着滚走
|
|
2360
|
+
const w = document.createElement('div');
|
|
2361
|
+
w.className = 'cpw';
|
|
2362
|
+
el.before(w); w.append(el, btn);
|
|
2363
|
+
}
|
|
2364
|
+
el._cpBtn = btn;
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
let _cpPending = false;
|
|
2368
|
+
new MutationObserver(() => {
|
|
2369
|
+
if (_cpPending) return;
|
|
2370
|
+
_cpPending = true;
|
|
2371
|
+
requestAnimationFrame(() => { _cpPending = false; attachCopyBtns(); });
|
|
2372
|
+
}).observe(chatArea, { childList: true, subtree: true });
|
|
2373
|
+
|
|
2316
2374
|
// 工具输出局部/全部显示。默认跟设置 rawClip;用户点过按钮的块记在 dataset.open,内容追加时保持。
|
|
2317
2375
|
// 只有内容真的超出 8 行高度才出按钮;回放时元素可能还在 fragment 里没上屏,等上屏再量。
|
|
2318
2376
|
function rawClipSync(msg, tries = 0) {
|
|
@@ -2324,13 +2382,7 @@
|
|
|
2324
2382
|
cp.className = 'raw-copy';
|
|
2325
2383
|
cp.textContent = '复制';
|
|
2326
2384
|
cp.title = '复制全部输出';
|
|
2327
|
-
cp.onclick = async () =>
|
|
2328
|
-
const ok = await copyText(block.textContent.replace(/\n+$/, ''));
|
|
2329
|
-
cp.textContent = ok ? '已复制' : '复制失败';
|
|
2330
|
-
cp.classList.add('done');
|
|
2331
|
-
clearTimeout(cp._t);
|
|
2332
|
-
cp._t = setTimeout(() => { cp.textContent = '复制'; cp.classList.remove('done'); }, 1500);
|
|
2333
|
-
};
|
|
2385
|
+
cp.onclick = async () => copyFlash(cp, await copyText(block.textContent.replace(/\n+$/, '')));
|
|
2334
2386
|
msg.appendChild(cp);
|
|
2335
2387
|
}
|
|
2336
2388
|
const clip = msg.dataset.open ? msg.dataset.open !== '1' : settings.rawClip !== false;
|
|
@@ -2906,9 +2958,12 @@
|
|
|
2906
2958
|
|
|
2907
2959
|
chatArea.addEventListener('scroll', () => {
|
|
2908
2960
|
// 用户主动往上翻 → 禁用自动 trim,避免被删
|
|
2909
|
-
|
|
2961
|
+
const dist = chatArea.scrollHeight - chatArea.clientHeight - chatArea.scrollTop;
|
|
2962
|
+
if (dist > 100) {
|
|
2910
2963
|
_userScrolledUp = true;
|
|
2911
|
-
|
|
2964
|
+
// 离底部超过 300px(翻看历史 / ☰ 定位跳到旧消息)→ 露出「↓ 回到底部」,一键回来
|
|
2965
|
+
if (dist > 300) setBottomBtn(true);
|
|
2966
|
+
} else if (dist < 20) {
|
|
2912
2967
|
// 回到接近底部 → 恢复 trim + 隐藏新消息提示
|
|
2913
2968
|
_userScrolledUp = false;
|
|
2914
2969
|
hideNewMsgHint();
|
|
@@ -3202,7 +3257,7 @@
|
|
|
3202
3257
|
// 输入框里打出 @词(@ 后无空白)时浮出文件搜索层:递归搜会话目录、
|
|
3203
3258
|
// 最近修改优先;选中把 @词 替换成真实相对路径。「让 AI 读某个文件」
|
|
3204
3259
|
// 不再需要手打文件名。也可在 📄 文件面板点「引用」达到同样效果。
|
|
3205
|
-
const _mention = { open: false, start: -1, items: [], idx: 0 };
|
|
3260
|
+
const _mention = { open: false, start: -1, items: [], idx: 0, browse: null, seq: 0, lock: '', token: null };
|
|
3206
3261
|
const _mentionBox = document.createElement('div');
|
|
3207
3262
|
_mentionBox.id = 'mention-box';
|
|
3208
3263
|
document.body.appendChild(_mentionBox);
|
|
@@ -3212,6 +3267,10 @@
|
|
|
3212
3267
|
function mentionToken() {
|
|
3213
3268
|
const pos = cmdInput.selectionStart ?? cmdInput.value.length;
|
|
3214
3269
|
const val = cmdInput.value;
|
|
3270
|
+
// 逐层进了带空格的文件夹(@My Docs/):认我们自己写进去的路径前缀,不按空白截断
|
|
3271
|
+
const L = _mention.open && _mention.lock;
|
|
3272
|
+
if (L && val[_mention.start] === '@' && val.startsWith(L, _mention.start + 1) && pos >= _mention.start + 1 + L.length
|
|
3273
|
+
&& !/\s/.test(val.slice(_mention.start + 1 + L.length, pos))) return { start: _mention.start };
|
|
3215
3274
|
const from = val.lastIndexOf('@', Math.max(0, pos - 1));
|
|
3216
3275
|
if (from < 0) return null;
|
|
3217
3276
|
if (from > 0 && /[A-Za-z0-9._%-]/.test(val[from - 1])) return null; // 邮箱等普通文本不触发
|
|
@@ -3221,24 +3280,55 @@
|
|
|
3221
3280
|
|
|
3222
3281
|
function mentionClose() {
|
|
3223
3282
|
_mention.open = false;
|
|
3283
|
+
_mention.browse = null;
|
|
3224
3284
|
_mention.items = [];
|
|
3285
|
+
_mention.lock = '';
|
|
3286
|
+
_mention.token = null;
|
|
3225
3287
|
_mentionBox.style.display = 'none';
|
|
3226
3288
|
}
|
|
3227
3289
|
|
|
3290
|
+
// 仿 Claude Code:@ 后是路径形态(含 / 或 \、C:、~)→ 按路径逐层浏览(本机任意目录,成员限本项目/绑定目录);
|
|
3291
|
+
// 只打 @ = 从本项目第一层开始浏览;@ 后打了普通字 = 本项目递归搜索。
|
|
3292
|
+
function mentionPathParts(token) {
|
|
3293
|
+
const t = token.replace(/\\/g, '/');
|
|
3294
|
+
if (/^[A-Za-z]:$/.test(t)) return { dir: t + '/', q: '' };
|
|
3295
|
+
if (t === '~') return { dir: '~/', q: '' };
|
|
3296
|
+
const i = t.lastIndexOf('/');
|
|
3297
|
+
return i < 0 ? null : { dir: t.slice(0, i + 1), q: t.slice(i + 1) };
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3228
3300
|
function mentionRowHTML(f, i) {
|
|
3229
|
-
const
|
|
3230
|
-
|
|
3301
|
+
const on = i === _mention.idx ? 'on' : '';
|
|
3302
|
+
if (f.act === 'up') return `<div class="mi ${on}" data-i="${i}"><span class="m-ico">⬆</span><span class="m-name">上一级</span></div>`;
|
|
3303
|
+
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>`;
|
|
3304
|
+
const path = f.label || f.name;
|
|
3305
|
+
const cut = f.label ? -1 : path.lastIndexOf('/');
|
|
3231
3306
|
const dir = cut >= 0 ? path.slice(0, cut + 1) : '';
|
|
3232
3307
|
const name = cut >= 0 ? path.slice(cut + 1) : path;
|
|
3233
|
-
const ico = f.dir ? '📁' : (isEditableFile(name) ? '📝' : '📄');
|
|
3234
|
-
const hint = f.dir ? (f.abs ? '项目外目录' : '文件夹') : (f.abs ? '项目外' : '引用');
|
|
3235
|
-
return `<div class="mi ${
|
|
3308
|
+
const ico = f.dir ? (f.drive ? '💽' : '📁') : (isEditableFile(name) ? '📝' : '📄');
|
|
3309
|
+
const hint = f.nav ? '进入 ›' : f.dir ? (f.abs ? '项目外目录' : '文件夹') : (f.abs ? '项目外' : '引用');
|
|
3310
|
+
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>`;
|
|
3236
3311
|
}
|
|
3237
3312
|
|
|
3238
3313
|
function mentionRender() {
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3314
|
+
const b = _mention.browse;
|
|
3315
|
+
let html = '';
|
|
3316
|
+
if (b) {
|
|
3317
|
+
const crumb = b.rel ? '本项目' + (b.dir ? ' / ' + b.dir.replace(/\/+$/, '').split('/').join(' / ') : '')
|
|
3318
|
+
: (b.abs === '/' && b.win ? '本机磁盘' : b.abs || b.dir);
|
|
3319
|
+
html += `<div class="mi-head">📂 ${escHtml(crumb)}</div>`;
|
|
3320
|
+
}
|
|
3321
|
+
_mention.items.forEach((f, i) => { html += mentionRowHTML(f, i); });
|
|
3322
|
+
if (b?.err) html += `<div class="mi-empty" style="color:var(--red)">${escHtml(b.err)}</div>`;
|
|
3323
|
+
else if (!_mention.items.some(f => !f.act)) html += `<div class="mi-empty">${b && !b.q ? '这个文件夹是空的' : '没有匹配的文件'}</div>`;
|
|
3324
|
+
if (b && !b.err) {
|
|
3325
|
+
const tips = [];
|
|
3326
|
+
if (_mention.items.some(f => f.nav)) tips.push('回车进入文件夹 · Tab 直接引用文件夹');
|
|
3327
|
+
if (!b.dir && _myRole !== 'admin') tips.push('打字搜索本项目');
|
|
3328
|
+
else if (!b.dir) tips.push('打字搜索本项目 · 项目外打 ' + (b.win ? '@D: 或 @\\' : '@/') + '、@~/ 逐层浏览');
|
|
3329
|
+
if (tips.length) html += `<div class="mi-tip">${escHtml(tips.join(' · '))}</div>`;
|
|
3330
|
+
}
|
|
3331
|
+
_mentionBox.innerHTML = html;
|
|
3242
3332
|
const r = cmdInput.getBoundingClientRect();
|
|
3243
3333
|
_mentionBox.style.left = Math.max(8, r.left) + 'px';
|
|
3244
3334
|
_mentionBox.style.width = Math.max(r.width, 240) + 'px';
|
|
@@ -3250,11 +3340,12 @@
|
|
|
3250
3340
|
|
|
3251
3341
|
function mentionSearch(token) {
|
|
3252
3342
|
clearTimeout(_mentionTimer);
|
|
3343
|
+
const seq = _mention.seq;
|
|
3253
3344
|
_mentionTimer = setTimeout(async () => {
|
|
3254
3345
|
try {
|
|
3255
3346
|
const r = await fetch(`/api/files/${SESSION_ID}/search?q=${encodeURIComponent(token)}`);
|
|
3256
3347
|
const d = await r.json();
|
|
3257
|
-
if (!_mention.open || !d.ok) return;
|
|
3348
|
+
if (seq !== _mention.seq || !_mention.open || !d.ok) return;
|
|
3258
3349
|
_mention.items = d.files || [];
|
|
3259
3350
|
_mention.idx = 0;
|
|
3260
3351
|
mentionRender();
|
|
@@ -3262,17 +3353,81 @@
|
|
|
3262
3353
|
}, 140);
|
|
3263
3354
|
}
|
|
3264
3355
|
|
|
3356
|
+
// 按路径列一层:dir 为 @ 后最后一个斜杠及之前(原样,服务端解析 ~ / C: / 相对路径),q 为最后一段
|
|
3357
|
+
function mentionList(dir, q) {
|
|
3358
|
+
clearTimeout(_mentionTimer);
|
|
3359
|
+
const seq = _mention.seq;
|
|
3360
|
+
_mentionTimer = setTimeout(async () => {
|
|
3361
|
+
try {
|
|
3362
|
+
const r = await fetch(`/api/files/${SESSION_ID}/at-ls?dir=${encodeURIComponent(dir)}&q=${encodeURIComponent(q)}`);
|
|
3363
|
+
const d = await r.json();
|
|
3364
|
+
if (seq !== _mention.seq || !_mention.open) return;
|
|
3365
|
+
if (!d.ok) { _mention.browse = { dir, q, err: d.error || '读取失败' }; _mention.items = []; mentionRender(); return; }
|
|
3366
|
+
// 插入输入框的路径:本项目内用相对路径;项目外一律绝对路径(~ 已由服务端展开),AI 直接按它读
|
|
3367
|
+
const absBase = d.abs === '/' ? '' : d.abs.replace(/\/+$/, '');
|
|
3368
|
+
const pickName = (label) => d.rel ? dir + label : (d.abs === '/' && d.win ? label : absBase + '/' + label);
|
|
3369
|
+
const items = [];
|
|
3370
|
+
if (!q && dir) {
|
|
3371
|
+
items.push({ act: 'up' });
|
|
3372
|
+
if (!(d.abs === '/' && d.win)) items.push({ act: 'self', name: d.rel ? dir.replace(/\/+$/, '') : absBase || '/', dir: true, abs: !d.rel }); // 盘符列表本身不能引用
|
|
3373
|
+
}
|
|
3374
|
+
d.entries.forEach(e => items.push({ label: e.label, name: pickName(e.label), dir: e.dir, nav: e.dir, abs: !d.rel, drive: d.abs === '/' && d.win, typed: dir + e.label + '/' }));
|
|
3375
|
+
_mention.browse = { dir, q, ...d };
|
|
3376
|
+
_mention.items = items;
|
|
3377
|
+
_mention.idx = Math.max(0, items.findIndex(f => !f.act)); // 默认落在第一个条目上,不是「上一级」
|
|
3378
|
+
mentionRender();
|
|
3379
|
+
} catch { mentionClose(); }
|
|
3380
|
+
}, 90);
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3265
3383
|
function mentionUpdate() {
|
|
3266
3384
|
const t = mentionToken();
|
|
3267
3385
|
if (!t) return mentionClose();
|
|
3386
|
+
const token = cmdInput.value.slice(t.start + 1, cmdInput.selectionStart ?? cmdInput.value.length);
|
|
3387
|
+
const same = _mention.open && _mention.start === t.start && _mention.token === token;
|
|
3388
|
+
if (_mention.start !== t.start) _mention.lock = '';
|
|
3268
3389
|
_mention.open = true;
|
|
3269
3390
|
_mention.start = t.start;
|
|
3270
|
-
|
|
3391
|
+
if (same) return; // keyup 等事件会反复进来,token 没变不重复请求
|
|
3392
|
+
_mention.token = token;
|
|
3393
|
+
_mention.seq++;
|
|
3394
|
+
const pp = token ? mentionPathParts(token) : { dir: '', q: '' };
|
|
3395
|
+
if (pp) return mentionList(pp.dir, pp.q);
|
|
3396
|
+
_mention.browse = null;
|
|
3397
|
+
mentionSearch(token);
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
// 把 @ 后面(到光标)替换成新的路径并继续浏览——「进入文件夹 / 上一级」都是改输入框里的路径,和手打等价
|
|
3401
|
+
function mentionGoto(tok) {
|
|
3402
|
+
const pos = cmdInput.selectionStart ?? cmdInput.value.length;
|
|
3403
|
+
const before = cmdInput.value.slice(0, _mention.start + 1);
|
|
3404
|
+
cmdInput.value = before + tok + cmdInput.value.slice(pos);
|
|
3405
|
+
const caret = (before + tok).length;
|
|
3406
|
+
cmdInput.setSelectionRange(caret, caret);
|
|
3407
|
+
_mention.lock = /\s/.test(tok) ? tok : '';
|
|
3408
|
+
cmdInput.focus();
|
|
3409
|
+
mentionUpdate();
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
// 回车/点击:文件夹进入、上一级返回、其余引用;Tab = 文件夹也直接引用
|
|
3413
|
+
function mentionActivate(f, pickDir) {
|
|
3414
|
+
if (!f) return;
|
|
3415
|
+
const b = _mention.browse;
|
|
3416
|
+
if (f.act === 'up') {
|
|
3417
|
+
if (b.rel) {
|
|
3418
|
+
const segs = b.dir.split('/').filter(Boolean);
|
|
3419
|
+
return mentionGoto(!segs.length || segs[segs.length - 1] === '..' ? b.dir + '../' : (segs.pop(), segs.length ? segs.join('/') + '/' : ''));
|
|
3420
|
+
}
|
|
3421
|
+
if (!b.parent) return;
|
|
3422
|
+
return mentionGoto(b.parent === '/' ? '/' : b.parent.replace(/\/+$/, '') + '/');
|
|
3423
|
+
}
|
|
3424
|
+
if (f.nav && !pickDir) return mentionGoto(f.typed);
|
|
3425
|
+
mentionPick(f);
|
|
3271
3426
|
}
|
|
3272
3427
|
|
|
3273
3428
|
function mentionPick(f) {
|
|
3274
3429
|
// 目录带尾斜杠插入(「迁移到 docs/」一眼看出是文件夹),文件带空格结尾
|
|
3275
|
-
const path = f.name + (f.dir ? '/' : '');
|
|
3430
|
+
const path = f.name.replace(/\/+$/, '') + (f.dir ? '/' : '');
|
|
3276
3431
|
const pos = cmdInput.selectionStart ?? cmdInput.value.length;
|
|
3277
3432
|
const before = cmdInput.value.slice(0, _mention.start);
|
|
3278
3433
|
const after = cmdInput.value.slice(pos);
|
|
@@ -3301,7 +3456,7 @@
|
|
|
3301
3456
|
if (!d) return;
|
|
3302
3457
|
if (Math.abs(e.clientX - d.x) >= 8 || Math.abs(e.clientY - d.y) >= 8) return; // 滚动手势
|
|
3303
3458
|
const row = e.pointerType === 'touch' ? d.row : e.target.closest('.mi[data-i]');
|
|
3304
|
-
if (row)
|
|
3459
|
+
if (row) mentionActivate(_mention.items[Number(row.dataset.i)]);
|
|
3305
3460
|
});
|
|
3306
3461
|
_mentionBox.addEventListener('pointercancel', () => { _mentionDown = null; });
|
|
3307
3462
|
window.addEventListener('resize', () => { if (_mention.open) mentionClose(); });
|
|
@@ -3315,7 +3470,7 @@
|
|
|
3315
3470
|
e.preventDefault(); _mention.idx = (_mention.idx - 1 + _mention.items.length) % _mention.items.length; mentionRender(); return;
|
|
3316
3471
|
}
|
|
3317
3472
|
if ((e.key === 'Enter' || e.key === 'Tab') && _mention.items.length && !e.isComposing && e.keyCode !== 229) {
|
|
3318
|
-
e.preventDefault();
|
|
3473
|
+
e.preventDefault(); mentionActivate(_mention.items[_mention.idx], e.key === 'Tab'); return; // 补全层开着时回车=选中/进入,不是发送
|
|
3319
3474
|
}
|
|
3320
3475
|
if (e.key === 'Escape') { e.preventDefault(); mentionClose(); return; } // 先关补全,别误触中断 AI
|
|
3321
3476
|
}
|
|
@@ -3391,10 +3546,10 @@
|
|
|
3391
3546
|
box.innerHTML = `
|
|
3392
3547
|
<div style="font-size:1rem;font-weight:700;margin-bottom:0.5rem">💬 会话怎么用</div>
|
|
3393
3548
|
<b>发消息</b>:像聊天一样把需求说清楚即可;触屏回车=换行,用发送键发送。<br>
|
|
3394
|
-
<b>让它读文件</b>:打 <b>@</b>
|
|
3549
|
+
<b>让它读文件</b>:打 <b>@</b> 从本项目逐层浏览、打字搜索;项目外打 <b>@D:</b>、<b>@/</b>、<b>@~/</b> 按路径逐层浏览;选中插路径;或点「文件」面板的 <b>@</b> 引用。<br>
|
|
3395
3550
|
<b>工具/思考</b>:AI 干活的过程默认折叠,点「🔧 N 个工具调用」展开看命令与输出。<br>
|
|
3396
3551
|
<b>AI 忙时</b>:输入框变「存草稿」,先记着干完取回;勾自动接力会自动发出。<br>
|
|
3397
|
-
<b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰
|
|
3552
|
+
<b>定位旧消息</b>:电脑上鼠标移到聊天区,右下角 ☰ 可列出你发过的消息点击跳转;看完点底部「↓ 回到底部」一键回来。<br>
|
|
3398
3553
|
<b>常用按钮</b>:模型/计划/压缩/恢复/清除/密钥/护栏/提交——普通用户可在设置→快捷栏精简。<br>
|
|
3399
3554
|
<span style="color:var(--muted)">更多说明见首页顶栏 ❓ 帮助。</span>`;
|
|
3400
3555
|
const closeBtn = document.createElement('button');
|
|
@@ -3434,7 +3589,7 @@
|
|
|
3434
3589
|
const d = await r.json();
|
|
3435
3590
|
if (d.ok) {
|
|
3436
3591
|
closeFilePanel();
|
|
3437
|
-
addStatusMessage('📂 已在本机打开文件夹 ' + (d.cwd || ''));
|
|
3592
|
+
addStatusMessage((d.via === 'task' ? '📂 已在本机桌面打开文件夹(myhi 在后台会话运行,经计划任务打开)' : '📂 已在本机打开文件夹 ') + (d.cwd || ''));
|
|
3438
3593
|
} else {
|
|
3439
3594
|
addStatusMessage('打开失败: ' + (d.error || '') + '(若你是远程访问本面板,此按钮本就不生效)');
|
|
3440
3595
|
}
|
|
@@ -4072,8 +4227,16 @@
|
|
|
4072
4227
|
if (_userScrolledUp) { showNewMsgHint(); return; }
|
|
4073
4228
|
requestAnimationFrame(() => { chatArea.scrollTop = chatArea.scrollHeight; });
|
|
4074
4229
|
}
|
|
4075
|
-
|
|
4076
|
-
function
|
|
4230
|
+
let _newBelow = false; // 翻看历史期间底下来了新消息 → 按钮文案变「↓ 新消息」
|
|
4231
|
+
function setBottomBtn(show) {
|
|
4232
|
+
const b = document.getElementById('new-msg-btn'); if (!b) return;
|
|
4233
|
+
b.textContent = _newBelow ? '↓ 新消息' : '↓ 回到底部';
|
|
4234
|
+
// 贴着聊天区底边上方:输入框/快捷栏高度随设备和内容变,写死 bottom 在手机上会压住输入框
|
|
4235
|
+
if (show) b.style.bottom = Math.max(12, window.innerHeight - chatArea.getBoundingClientRect().bottom + 12) + 'px';
|
|
4236
|
+
b.classList.toggle('show', show);
|
|
4237
|
+
}
|
|
4238
|
+
function showNewMsgHint() { _newBelow = true; setBottomBtn(true); }
|
|
4239
|
+
function hideNewMsgHint() { _newBelow = false; setBottomBtn(false); }
|
|
4077
4240
|
window.jumpToBottom = function() { _userScrolledUp = false; hideNewMsgHint(); requestAnimationFrame(() => { chatArea.scrollTop = chatArea.scrollHeight; }); };
|
|
4078
4241
|
function trimMessages() {
|
|
4079
4242
|
if (_userScrolledUp) return; // 用户在往上翻历史,禁用自动删
|