@wendongfly/myhi 1.3.210 → 1.3.213
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 +252 -60
- package/dist/chat.html.gz +0 -0
- package/dist/index.html +5 -4
- package/dist/index.html.gz +0 -0
- package/dist/index.js +1 -1
- package/dist/index.min.js +158 -155
- package/package.json +1 -1
package/dist/chat.html
CHANGED
|
@@ -267,8 +267,14 @@
|
|
|
267
267
|
#input-area.no-shortcuts { padding-bottom: max(0.5rem, env(safe-area-inset-bottom)); }
|
|
268
268
|
#input-box { background: var(--bg); border: 1px solid var(--border); border-radius: 12px; transition: border-color 0.2s; overflow: hidden; position: relative; }
|
|
269
269
|
#img-preview { display: none; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--surface2); background: var(--surface); }
|
|
270
|
-
#img-preview.show { display: flex; }
|
|
270
|
+
#img-preview.show { display: flex; padding-right: 2.8rem; } /* 右侧让出悬浮的 ☰ 按钮,免得挡住「全部移除」 */
|
|
271
271
|
#img-preview img { width: 48px; height: 48px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border); }
|
|
272
|
+
#img-preview-list { display: flex; gap: 6px; flex-shrink: 1; min-width: 0; overflow-x: auto; scrollbar-width: none; }
|
|
273
|
+
#img-preview .ip-item { position: relative; flex-shrink: 0; }
|
|
274
|
+
#img-preview .ip-ph { width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; border-radius: 6px; border: 1px solid var(--border); background: var(--surface2); font-size: 1.2rem; }
|
|
275
|
+
#img-preview .ip-x { position: absolute; top: -4px; right: -4px; width: 18px; height: 18px; border-radius: 50%; border: none; background: rgba(0,0,0,0.65); color: #fff; font-size: 12px; line-height: 18px; padding: 0; cursor: pointer; }
|
|
276
|
+
#img-pro { flex-shrink: 0; border: 1px solid var(--border); background: var(--surface2); color: var(--muted); border-radius: 12px; padding: 0.15rem 0.5rem; font-size: 0.7rem; cursor: pointer; white-space: nowrap; }
|
|
277
|
+
#img-pro.on { border-color: var(--purple); color: var(--purple); background: color-mix(in srgb, var(--purple) 10%, transparent); font-weight: 600; }
|
|
272
278
|
#img-preview .img-info { flex: 1; font-size: 0.78rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
273
279
|
#img-preview .img-remove { width: 24px; height: 24px; border-radius: 50%; border: none; background: var(--surface2); color: var(--muted); cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; flex-shrink: 0; }
|
|
274
280
|
#img-preview .img-remove:hover { background: var(--red); color: #fff; }
|
|
@@ -693,6 +699,50 @@
|
|
|
693
699
|
#fedit-sheet.action-sheet.open { align-items: center; }
|
|
694
700
|
#fedit-sheet .action-sheet-box { width: 94vw; max-width: 1200px; border-radius: 14px; padding-bottom: 0.75rem; }
|
|
695
701
|
#memory-content { flex: 1; min-height: 0; }
|
|
702
|
+
/* 方案审批:手机全屏,电脑居中宽屏 + 左侧目录 */
|
|
703
|
+
#plan-sheet .plan-box { max-width: none; height: 100dvh; max-height: 100dvh; border-radius: 0; display: flex; flex-direction: column; padding: 0.5rem 0.75rem max(0.6rem, env(safe-area-inset-bottom)); }
|
|
704
|
+
@media (min-width: 768px) {
|
|
705
|
+
#plan-sheet.action-sheet.open { align-items: center; }
|
|
706
|
+
#plan-sheet .plan-box { width: 96vw; max-width: 1280px; height: 92vh; border-radius: 14px; }
|
|
707
|
+
}
|
|
708
|
+
.plan-head { display: flex; align-items: center; gap: 0.4rem; padding: 0.2rem 0 0.5rem; border-bottom: 1px solid var(--surface2); flex-wrap: wrap; font-size: 0.9rem; }
|
|
709
|
+
.plan-stat { font-size: 0.72rem; color: var(--muted); }
|
|
710
|
+
.plan-hbtn { background: var(--surface2); border: 1px solid var(--border); color: var(--text); border-radius: 8px; padding: 0.3rem 0.6rem; font-size: 0.78rem; cursor: pointer; white-space: nowrap; }
|
|
711
|
+
.plan-hbtn:hover { border-color: var(--purple); }
|
|
712
|
+
#plan-ai-btn { color: var(--purple); font-weight: 600; }
|
|
713
|
+
.plan-main { flex: 1; min-height: 0; display: flex; gap: 0.6rem; position: relative; padding-top: 0.4rem; }
|
|
714
|
+
#plan-toc { width: 230px; flex-shrink: 0; overflow-y: auto; font-size: 0.78rem; border-right: 1px solid var(--surface2); padding-right: 0.4rem; scrollbar-width: thin; }
|
|
715
|
+
#plan-toc a { display: block; padding: 0.28rem 0.4rem; border-radius: 6px; color: var(--text2, var(--text)); text-decoration: none; cursor: pointer; line-height: 1.35; }
|
|
716
|
+
#plan-toc a:hover, #plan-toc a.on { background: var(--surface2); color: var(--purple); }
|
|
717
|
+
#plan-toc a.l3 { padding-left: 1.1rem; font-size: 0.74rem; } #plan-toc a.l4 { padding-left: 1.8rem; font-size: 0.72rem; color: var(--muted); }
|
|
718
|
+
.plan-toc-btn { display: none; }
|
|
719
|
+
@media (max-width: 767px) {
|
|
720
|
+
#plan-toc { display: none; position: absolute; z-index: 3; left: 0; top: 0.4rem; bottom: 0; width: 78%; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 0.4rem; box-shadow: 0 6px 24px rgba(0,0,0,0.3); }
|
|
721
|
+
#plan-toc.show { display: block; }
|
|
722
|
+
.plan-toc-btn { display: inline-block; }
|
|
723
|
+
}
|
|
724
|
+
.plan-scroll { flex: 1; min-width: 0; overflow-y: auto; overflow-x: hidden; padding: 0.2rem 0.5rem; scrollbar-width: thin; }
|
|
725
|
+
#plan-sheet-body pre { white-space: pre-wrap; word-break: break-word; }
|
|
726
|
+
#plan-sheet-body code { word-break: break-all; }
|
|
727
|
+
#plan-sheet-body table { display: block; overflow-x: auto; }
|
|
728
|
+
#plan-digest { border: 1px solid var(--purple); border-radius: 10px; padding: 0.5rem 0.75rem; margin: 0.2rem 0 0.8rem; background: color-mix(in srgb, var(--purple) 6%, transparent); font-size: 0.86rem; }
|
|
729
|
+
#plan-digest .pd-h { display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: var(--muted); margin-bottom: 0.3rem; }
|
|
730
|
+
.plan-foot { border-top: 1px solid var(--surface2); padding-top: 0.5rem; }
|
|
731
|
+
#plan-sheet-fb { min-height: 5.5em; max-height: 40vh; resize: vertical; margin-bottom: 0.45rem; line-height: 1.5; }
|
|
732
|
+
.plan-btns { display: flex; flex-direction: column; gap: 0.4rem; }
|
|
733
|
+
.plan-btns .action-sheet-cancel { margin: 0; }
|
|
734
|
+
@media (min-width: 768px) { .plan-btns { flex-direction: row; } .plan-btns .action-sheet-cancel { flex: 1; } }
|
|
735
|
+
.plan-t-short { display: none; }
|
|
736
|
+
@media (max-width: 767px) {
|
|
737
|
+
.plan-t-long { display: none; } .plan-t-short { display: inline; }
|
|
738
|
+
.plan-head { flex-wrap: nowrap; } .plan-stat { display: none; }
|
|
739
|
+
#plan-sheet-fb { min-height: 3.2em; }
|
|
740
|
+
/* 批准占一整行,另外两个并排,给方案多留阅读空间 */
|
|
741
|
+
.plan-btns { display: grid; grid-template-columns: 1fr 1fr; }
|
|
742
|
+
.plan-btns .action-sheet-cancel:first-child { grid-column: 1 / -1; }
|
|
743
|
+
}
|
|
744
|
+
#plan-quote-btn { display: none; position: fixed; z-index: 95; background: var(--purple); color: #fff; border: none; border-radius: 8px; padding: 0.4rem 0.75rem; font-size: 0.8rem; box-shadow: 0 4px 14px rgba(0,0,0,0.3); cursor: pointer; }
|
|
745
|
+
#plan-quote-btn.show { display: block; }
|
|
696
746
|
.slash-inp { width: 100%; padding: 0.65rem 0.8rem; background: var(--bg); border: 1px solid var(--border); border-radius: 10px; color: var(--text); font-size: 0.9rem; outline: none; }
|
|
697
747
|
.slash-inp:focus { border-color: var(--purple); }
|
|
698
748
|
|
|
@@ -801,9 +851,10 @@
|
|
|
801
851
|
<button class="img-remove" onclick="guideTipClose()" title="先不管">×</button>
|
|
802
852
|
</div>
|
|
803
853
|
<div id="img-preview">
|
|
804
|
-
<
|
|
805
|
-
<span class="img-info">已添加图片,输入文字后发送</span>
|
|
806
|
-
<button
|
|
854
|
+
<div id="img-preview-list"></div>
|
|
855
|
+
<span class="img-info" id="img-preview-info">已添加图片,输入文字后发送</span>
|
|
856
|
+
<button id="img-pro" style="display:none" onclick="toggleVisionPro()" title="重要截图(复杂标注的 bug 单、设计稿)改用「看图·高精度」模型识别,更准但更贵 / 更慢">🔍 高精度</button>
|
|
857
|
+
<button class="img-remove" onclick="clearPendingImage()" title="移除全部图片">×</button>
|
|
807
858
|
</div>
|
|
808
859
|
<div id="file-preview" style="display:none;align-items:center;gap:0.5rem;padding:0.5rem 0.75rem;border-bottom:1px solid var(--surface2);background:var(--surface)">
|
|
809
860
|
<span style="font-size:1.1rem;flex-shrink:0">📎</span>
|
|
@@ -1348,17 +1399,36 @@
|
|
|
1348
1399
|
</div>
|
|
1349
1400
|
</div>
|
|
1350
1401
|
<!-- 方案审批:计划模式下 AI 用 ExitPlanMode 提交方案 -->
|
|
1402
|
+
<!-- 长方案也要审得动:电脑上宽屏 + 左侧目录,手机全屏;「AI 帮我看重点」先出摘要;选中一段可引用到意见框 -->
|
|
1351
1403
|
<div id="plan-sheet" class="action-sheet">
|
|
1352
1404
|
<div class="action-sheet-backdrop" onclick="closePlanSheet()"></div>
|
|
1353
|
-
<div class="action-sheet-box
|
|
1354
|
-
<div class="
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1405
|
+
<div class="action-sheet-box plan-box">
|
|
1406
|
+
<div class="plan-head">
|
|
1407
|
+
<b>📋 <span class="plan-t-long">AI 提交了方案,请审批</span><span class="plan-t-short">方案审批</span></b>
|
|
1408
|
+
<span id="plan-stat" class="plan-stat"></span>
|
|
1409
|
+
<span style="flex:1"></span>
|
|
1410
|
+
<button class="plan-hbtn" id="plan-ai-btn" onclick="planDigest()" title="用「问一问」的模型先把方案读一遍:概括、会改什么、需要你确认的、风险、可以提的意见">✨ AI 帮我看重点</button>
|
|
1411
|
+
<button class="plan-hbtn plan-toc-btn" onclick="togglePlanToc()">☰ 目录</button>
|
|
1412
|
+
<button class="plan-hbtn" onclick="closePlanSheet()" title="稍后再看">✕</button>
|
|
1413
|
+
</div>
|
|
1414
|
+
<div class="plan-main">
|
|
1415
|
+
<nav id="plan-toc"></nav>
|
|
1416
|
+
<div class="plan-scroll msg-assistant" id="plan-scroll">
|
|
1417
|
+
<div id="plan-digest" style="display:none"></div>
|
|
1418
|
+
<div class="content" id="plan-sheet-body"></div>
|
|
1419
|
+
</div>
|
|
1420
|
+
</div>
|
|
1421
|
+
<div class="plan-foot">
|
|
1422
|
+
<textarea id="plan-sheet-fb" class="slash-inp" rows="3" placeholder="修改意见(可选):直接写;或在上面选中一段文字,点「💬 对这段提意见」把它引用过来,再写意见"></textarea>
|
|
1423
|
+
<div class="plan-btns">
|
|
1424
|
+
<button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600" onclick="approvePlan()">✅ 批准并执行</button>
|
|
1425
|
+
<button class="action-sheet-cancel" onclick="revisePlan()">✏️ 按意见修改方案</button>
|
|
1426
|
+
<button class="action-sheet-cancel" onclick="closePlanSheet()">稍后再看</button>
|
|
1427
|
+
</div>
|
|
1428
|
+
</div>
|
|
1360
1429
|
</div>
|
|
1361
1430
|
</div>
|
|
1431
|
+
<button id="plan-quote-btn" onmousedown="event.preventDefault()" onclick="planQuoteSel()">💬 对这段提意见</button>
|
|
1362
1432
|
<!-- 退出计划:按方案执行 / 仅退出 -->
|
|
1363
1433
|
<div id="planexit-sheet" class="action-sheet">
|
|
1364
1434
|
<div class="action-sheet-backdrop" onclick="closePlanExit()"></div>
|
|
@@ -2623,10 +2693,10 @@
|
|
|
2623
2693
|
if (syntheticUserKind(msg.content)) return;
|
|
2624
2694
|
// 看图转文字后发给模型的消息(对话记录里就是改写后的全文):气泡只显示原话,图片描述收进折叠块
|
|
2625
2695
|
let content = msg.content, vis = null;
|
|
2626
|
-
const vi = typeof content === 'string' ? content.
|
|
2696
|
+
const vi = typeof content === 'string' ? content.search(/\n\n(注意:(你当前用的模型不能直接看图|myhi 已用看图模型)/) : -1; // 新旧两种措辞
|
|
2627
2697
|
if (vi > 0) {
|
|
2628
2698
|
const tail = content.slice(vi);
|
|
2629
|
-
vis = { message: '', vision: { model: (tail.match(/看图模型 (\S+)
|
|
2699
|
+
vis = { message: '', vision: { model: (tail.match(/看图模型 (\S+) 把/) || [])[1] || '',
|
|
2630
2700
|
items: [...tail.matchAll(/【图片 ([^】]+)】\n([\s\S]*?)(?=\n\n【图片 |$)/g)].map(m => ({ path: m[1], text: m[2] })) } };
|
|
2631
2701
|
content = content.slice(0, vi);
|
|
2632
2702
|
}
|
|
@@ -3542,14 +3612,57 @@
|
|
|
3542
3612
|
});
|
|
3543
3613
|
|
|
3544
3614
|
// ── 待发送图片 ──────────────────────────────────
|
|
3545
|
-
|
|
3615
|
+
// 一次可以挂多张图(最多 MAX_IMAGES),一起发给 AI
|
|
3616
|
+
const MAX_IMAGES = 9;
|
|
3617
|
+
let _pendingImages = []; // [{ path, blobUrl }]
|
|
3618
|
+
let _visionPro = false; // 这批图用「看图·高精度」模型(设置里配了才显示开关)
|
|
3619
|
+
window.toggleVisionPro = function() { _visionPro = !_visionPro; renderPendingImages(); };
|
|
3546
3620
|
|
|
3547
3621
|
window.clearPendingImage = function() {
|
|
3548
|
-
if (
|
|
3549
|
-
|
|
3550
|
-
|
|
3622
|
+
for (const im of _pendingImages) if (im.blobUrl) URL.revokeObjectURL(im.blobUrl);
|
|
3623
|
+
_pendingImages = [];
|
|
3624
|
+
_visionPro = false;
|
|
3625
|
+
renderPendingImages();
|
|
3551
3626
|
cmdInput.placeholder = currentSession?.mode === 'agent' ? '输入消息...' : '输入命令...';
|
|
3552
3627
|
};
|
|
3628
|
+
window.removePendingImage = function(i) {
|
|
3629
|
+
const [im] = _pendingImages.splice(i, 1);
|
|
3630
|
+
if (im?.blobUrl) URL.revokeObjectURL(im.blobUrl);
|
|
3631
|
+
if (!_pendingImages.length) return clearPendingImage();
|
|
3632
|
+
renderPendingImages();
|
|
3633
|
+
};
|
|
3634
|
+
function renderPendingImages() {
|
|
3635
|
+
const n = _pendingImages.length;
|
|
3636
|
+
document.getElementById('img-preview').classList.toggle('show', n > 0);
|
|
3637
|
+
// 从草稿取回时只剩服务器路径、没有本地预览图 → 显示占位图标,免得破图
|
|
3638
|
+
document.getElementById('img-preview-list').innerHTML = _pendingImages.map((im, i) =>
|
|
3639
|
+
`<span class="ip-item">${im.blobUrl ? `<img src="${im.blobUrl}" alt="">` : '<span class="ip-ph">🖼</span>'}<button class="ip-x" onclick="removePendingImage(${i})" title="移除这张">×</button></span>`).join('');
|
|
3640
|
+
let sc = {}, mi = null; try { mi = _modelInfo; sc = mi?.scenes || {}; } catch { /* 页面初始化早期 _modelInfo 还没声明 */ }
|
|
3641
|
+
const info = document.getElementById('img-preview-info');
|
|
3642
|
+
info.textContent = (n <= 1 ? '已添加图片,输入文字后发送' : `已添加 ${n} 张图片(最多 ${MAX_IMAGES} 张),输入文字后一起发送`)
|
|
3643
|
+
+ (sc.vision || sc.visionPro ? ' · 标注尽量配文字说明' : '');
|
|
3644
|
+
// 看图模型对红框 / 箭头的指向常认错,对文字最可靠(看图接入指南 §4)
|
|
3645
|
+
info.title = sc.vision || sc.visionPro ? '截图标注首选文字(红字、①②③ 序号、「见下方说明」),看图模型提取最可靠;红框 / 高亮 / 箭头它能发现但常认错指的是谁,重要的指向请配文字' : '';
|
|
3646
|
+
const pro = document.getElementById('img-pro');
|
|
3647
|
+
pro.style.display = n && sc.visionPro && currentSession?.mode === 'agent' ? '' : 'none';
|
|
3648
|
+
pro.classList.toggle('on', _visionPro);
|
|
3649
|
+
pro.textContent = _visionPro ? '🔍 高精度 ✓' : '🔍 高精度';
|
|
3650
|
+
if (n && !mi) loadModelInfo().then(() => { if (_pendingImages.length) renderPendingImages(); });
|
|
3651
|
+
}
|
|
3652
|
+
// 消息里附件的说明:AI 按路径读取;多张图 / 图 + 文件一起时逐一列出(格式与草稿合并、看图转文字一致)
|
|
3653
|
+
function attachPrompt(imgs, file, text) {
|
|
3654
|
+
const parts = [];
|
|
3655
|
+
if (imgs.length === 1) parts.push(`用户上传了一张图片,路径是 ${imgs[0]},请用 Read 工具读取这张图片。`);
|
|
3656
|
+
else if (imgs.length > 1) parts.push(`用户上传了 ${imgs.length} 张图片,路径分别是 ${imgs.join('、')},请用 Read 工具逐一读取。`);
|
|
3657
|
+
if (file) parts.push(`用户上传了一个临时文件用于本次分析(不在项目文件树里),路径是 ${file.path}(原文件名 ${file.name}),请用合适的工具(如 Read)读取并分析它。`);
|
|
3658
|
+
if (!parts.length) return text;
|
|
3659
|
+
return parts.join('') + '用户的要求是:' + (text || (file && !imgs.length ? '分析这个文件' : imgs.length > 1 ? '描述这些图片的内容' : '描述图片内容'));
|
|
3660
|
+
}
|
|
3661
|
+
function attachDisplay(imgs, file, text) {
|
|
3662
|
+
if (imgs.length) return `📷${imgs.length > 1 ? '×' + imgs.length : ''}${file ? ' 📎 ' + file.name : ''} ${text || (imgs.length > 1 ? '分析这些图片' : '分析图片')}`;
|
|
3663
|
+
if (file) return `📎 ${file.name}${text ? ' ' + text : ''}`;
|
|
3664
|
+
return text;
|
|
3665
|
+
}
|
|
3553
3666
|
|
|
3554
3667
|
window.cancelAgent = function() {
|
|
3555
3668
|
if (currentSession?.mode === 'agent' && workState !== 'idle') {
|
|
@@ -3558,13 +3671,13 @@
|
|
|
3558
3671
|
};
|
|
3559
3672
|
|
|
3560
3673
|
function setPendingImage(path, blobUrl) {
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3674
|
+
if (_pendingImages.length >= MAX_IMAGES) {
|
|
3675
|
+
if (blobUrl) URL.revokeObjectURL(blobUrl);
|
|
3676
|
+
addStatusMessage(`一次最多 ${MAX_IMAGES} 张图片,这张没有加上`);
|
|
3677
|
+
return;
|
|
3678
|
+
}
|
|
3679
|
+
_pendingImages.push({ path, blobUrl });
|
|
3680
|
+
renderPendingImages();
|
|
3568
3681
|
cmdInput.placeholder = '输入文字描述后发送,或直接发送分析图片...';
|
|
3569
3682
|
cmdInput.focus();
|
|
3570
3683
|
}
|
|
@@ -3755,7 +3868,7 @@
|
|
|
3755
3868
|
function sendCommand() {
|
|
3756
3869
|
const text = cmdInput.value.trim();
|
|
3757
3870
|
if (GUIDE_REMEMBER_RE.test(text)) _guideRemember = true; // 「以后都… / 记住…」:这一轮干完建议写进项目说明
|
|
3758
|
-
if (!text && !
|
|
3871
|
+
if (!text && !_pendingImages.length && !_pendingFile) return;
|
|
3759
3872
|
|
|
3760
3873
|
// AI 还在跑这一轮(可能正开着子代理,整轮 result 未到,服务端 isBusy 会拒收):
|
|
3761
3874
|
// 不再把输入连同刚上传的图片一起丢掉,转存草稿,本轮干完再取回。
|
|
@@ -3776,33 +3889,27 @@
|
|
|
3776
3889
|
}
|
|
3777
3890
|
endStream();
|
|
3778
3891
|
|
|
3779
|
-
const
|
|
3892
|
+
const imgPaths = _pendingImages.map(im => im.path);
|
|
3780
3893
|
const filePath = _pendingFile?.path || null;
|
|
3781
3894
|
const fileName = _pendingFile?.name || '';
|
|
3782
3895
|
const userText = text;
|
|
3783
|
-
const displayText =
|
|
3784
|
-
: filePath ? `📎 ${fileName}${userText ? ' ' + userText : ''}`
|
|
3785
|
-
: text;
|
|
3896
|
+
const displayText = attachDisplay(imgPaths, _pendingFile, userText);
|
|
3786
3897
|
|
|
3787
3898
|
addInputMessage(displayText);
|
|
3788
3899
|
|
|
3789
3900
|
if (currentSession?.mode === 'agent') {
|
|
3790
|
-
const prompt =
|
|
3791
|
-
|
|
3792
|
-
: filePath
|
|
3793
|
-
? `用户上传了一个临时文件用于本次分析(不在项目文件树里),路径是 ${filePath}(原文件名 ${fileName}),请用合适的工具(如 Read)读取并分析它。用户的要求是:${userText || '分析这个文件'}`
|
|
3794
|
-
: text;
|
|
3795
|
-
socket.emit('agent:query', { prompt });
|
|
3901
|
+
const prompt = attachPrompt(imgPaths, _pendingFile, userText);
|
|
3902
|
+
socket.emit('agent:query', { prompt, visionPro: imgPaths.length > 0 && _visionPro });
|
|
3796
3903
|
// 本地 slash(/model /clear /plan /resume /rename)只回一条 info、无 result,不进 thinking 态
|
|
3797
3904
|
if (!LOCAL_SLASH_RE.test(text)) showThinking();
|
|
3798
3905
|
} else {
|
|
3799
|
-
const attach =
|
|
3906
|
+
const attach = [...imgPaths, filePath].filter(Boolean).join(' ');
|
|
3800
3907
|
const input = attach
|
|
3801
3908
|
? (userText ? `${userText} ${attach}` : attach)
|
|
3802
3909
|
: text;
|
|
3803
3910
|
socket.emit('input', input + '\r');
|
|
3804
3911
|
}
|
|
3805
|
-
cmdHistory.unshift(
|
|
3912
|
+
cmdHistory.unshift(imgPaths.length ? `[图片${imgPaths.length > 1 ? '×' + imgPaths.length : ''}] ${userText}` : filePath ? `[文件] ${fileName} ${userText}` : text);
|
|
3806
3913
|
if (cmdHistory.length > 100) cmdHistory.pop();
|
|
3807
3914
|
historyIdx = -1;
|
|
3808
3915
|
cmdInput.value = '';
|
|
@@ -3834,17 +3941,11 @@
|
|
|
3834
3941
|
}
|
|
3835
3942
|
window.injectFromInput = function() {
|
|
3836
3943
|
const text = cmdInput.value.trim();
|
|
3837
|
-
if (!text && !
|
|
3944
|
+
if (!text && !_pendingImages.length && !_pendingFile) { addStatusMessage('先在输入框里写下要补充的话,再点 ⚡ 插话'); cmdInput.focus(); return; }
|
|
3838
3945
|
if (!canInjectNow()) { sendCommand(); return; }
|
|
3839
|
-
const
|
|
3840
|
-
const
|
|
3841
|
-
const
|
|
3842
|
-
const display = imgPath ? `📷 ${text || '分析图片'}` : filePath ? `📎 ${fileName}${text ? ' ' + text : ''}` : text;
|
|
3843
|
-
const prompt = imgPath
|
|
3844
|
-
? `用户上传了一张图片,路径是 ${imgPath},请用 Read 工具读取这张图片。用户的要求是:${text || '描述图片内容'}`
|
|
3845
|
-
: filePath
|
|
3846
|
-
? `用户上传了一个临时文件用于本次分析(不在项目文件树里),路径是 ${filePath}(原文件名 ${fileName}),请用合适的工具(如 Read)读取并分析它。用户的要求是:${text || '分析这个文件'}`
|
|
3847
|
-
: text;
|
|
3946
|
+
const imgPaths = _pendingImages.map(im => im.path);
|
|
3947
|
+
const display = attachDisplay(imgPaths, _pendingFile, text);
|
|
3948
|
+
const prompt = attachPrompt(imgPaths, _pendingFile, text);
|
|
3848
3949
|
injectEmit({ prompt, display }, () => {
|
|
3849
3950
|
cmdInput.value = ''; cmdInput.style.height = 'auto';
|
|
3850
3951
|
clearPendingImage(); clearPendingFile();
|
|
@@ -5581,12 +5682,100 @@
|
|
|
5581
5682
|
if (_askCtlId) document.getElementById('ask-sheet').classList.add('open');
|
|
5582
5683
|
};
|
|
5583
5684
|
// ── 方案审批 ──
|
|
5685
|
+
let _planDigest = { id: null, text: '', state: '' }; // AI 重点速览:按方案 id 缓存,关了再开不重复花钱
|
|
5686
|
+
let _planDigestReq = null;
|
|
5584
5687
|
window.openPlanSheet = function() {
|
|
5585
5688
|
if (!_planCtl) return;
|
|
5586
|
-
|
|
5689
|
+
const plan = _planCtl.plan || '(方案为空)';
|
|
5690
|
+
const body = document.getElementById('plan-sheet-body');
|
|
5691
|
+
body.innerHTML = renderMarkdown(plan);
|
|
5692
|
+
// 目录:方案里的标题(# ~ ####),点了跳到对应位置
|
|
5693
|
+
const hs = [...body.querySelectorAll('h1,h2,h3,h4')];
|
|
5694
|
+
const minL = Math.min(...hs.map(h => +h.tagName[1]), 9);
|
|
5695
|
+
document.getElementById('plan-toc').innerHTML = hs.length < 2 ? '<div style="color:var(--muted);padding:0.4rem">这份方案没有分节标题</div>'
|
|
5696
|
+
: hs.map((h, i) => { h.id = 'plan-h-' + i; return `<a class="l${Math.min(4, h.tagName[1] - minL + 2)}" data-i="${i}">${escHtml(h.textContent.trim())}</a>`; }).join('');
|
|
5697
|
+
document.getElementById('plan-toc').querySelectorAll('a').forEach(a => a.onclick = () => {
|
|
5698
|
+
document.getElementById('plan-h-' + a.dataset.i)?.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
|
5699
|
+
document.getElementById('plan-toc').classList.remove('show');
|
|
5700
|
+
});
|
|
5701
|
+
const chars = plan.replace(/\s/g, '').length;
|
|
5702
|
+
document.getElementById('plan-stat').textContent = `约 ${chars} 字${hs.length ? ` · ${hs.length} 节` : ''} · 约 ${Math.max(1, Math.round(chars / 500))} 分钟读完`;
|
|
5703
|
+
document.getElementById('plan-scroll').scrollTop = 0;
|
|
5704
|
+
renderPlanDigest();
|
|
5587
5705
|
document.getElementById('plan-sheet').classList.add('open');
|
|
5588
5706
|
};
|
|
5589
|
-
window.closePlanSheet = function() {
|
|
5707
|
+
window.closePlanSheet = function() {
|
|
5708
|
+
document.getElementById('plan-sheet').classList.remove('open');
|
|
5709
|
+
document.getElementById('plan-quote-btn').classList.remove('show');
|
|
5710
|
+
renderPendingBar();
|
|
5711
|
+
};
|
|
5712
|
+
window.togglePlanToc = function() { document.getElementById('plan-toc').classList.toggle('show'); };
|
|
5713
|
+
// 目录高亮:滚到哪一节就亮哪一节
|
|
5714
|
+
document.getElementById('plan-scroll').addEventListener('scroll', () => {
|
|
5715
|
+
const sc = document.getElementById('plan-scroll'), top = sc.getBoundingClientRect().top + 40;
|
|
5716
|
+
let cur = null;
|
|
5717
|
+
sc.querySelectorAll('#plan-sheet-body [id^="plan-h-"]').forEach(h => { if (h.getBoundingClientRect().top <= top) cur = h.id.slice(7); });
|
|
5718
|
+
document.querySelectorAll('#plan-toc a').forEach(a => a.classList.toggle('on', a.dataset.i === cur));
|
|
5719
|
+
}, { passive: true });
|
|
5720
|
+
// 意见框按内容长高(最多 40% 屏高)
|
|
5721
|
+
document.getElementById('plan-sheet-fb').addEventListener('input', (e) => {
|
|
5722
|
+
const t = e.target; t.style.height = 'auto'; t.style.height = Math.min(t.scrollHeight + 2, innerHeight * 0.4) + 'px';
|
|
5723
|
+
});
|
|
5724
|
+
// ✨ AI 帮我看重点:走「问一问」那条通道(问一问 / 解释的场景模型),不进当前会话、不打扰 AI
|
|
5725
|
+
const PLAN_DIGEST_Q = '请帮用户审这份方案。用中文、简洁,按下面的结构输出,每项 1~5 条要点,尽量引用方案里的原词方便用户定位;没有的项写「无」,不要复述全文:\n' +
|
|
5726
|
+
'### 一句话概括\n### 会改动什么(模块 / 文件 / 数据库 / 接口 / 配置)\n### 需要你确认或提供的(凭据、权限、取舍决定等)\n### 风险与容易出错的地方\n### 可以提的修改意见 / 建议追问';
|
|
5727
|
+
function renderPlanDigest() {
|
|
5728
|
+
const box = document.getElementById('plan-digest'), btn = document.getElementById('plan-ai-btn');
|
|
5729
|
+
const mine = _planCtl && _planDigest.id === _planCtl.id;
|
|
5730
|
+
if (!mine || !_planDigest.state) { box.style.display = 'none'; btn.textContent = '✨ AI 帮我看重点'; btn.disabled = false; return; }
|
|
5731
|
+
box.style.display = '';
|
|
5732
|
+
const busy = _planDigest.state === 'busy';
|
|
5733
|
+
btn.textContent = busy ? '⏳ 正在看…' : '✨ 重新生成'; btn.disabled = busy;
|
|
5734
|
+
box.innerHTML = `<div class="pd-h"><b style="color:var(--purple)">✨ 重点速览</b><span>由「问一问」模型生成,仅供参考,以下面方案原文为准</span>${busy ? '' : '<span style="flex:1"></span><a style="cursor:pointer;color:var(--muted)" onclick="document.getElementById(\'plan-digest\').style.display=\'none\'">收起</a>'}</div>` +
|
|
5735
|
+
(_planDigest.err ? `<div style="color:var(--red)">没能生成:${escHtml(_planDigest.err)}</div>` : '') +
|
|
5736
|
+
`<div class="content">${_planDigest.text ? renderMarkdown(_planDigest.text) : '<span style="color:var(--muted)">AI 正在读方案…</span>'}</div>`;
|
|
5737
|
+
}
|
|
5738
|
+
window.planDigest = function() {
|
|
5739
|
+
if (!_planCtl) return;
|
|
5740
|
+
_planDigestReq = 'plan-' + Date.now();
|
|
5741
|
+
_planDigest = { id: _planCtl.id, text: '', state: 'busy' };
|
|
5742
|
+
renderPlanDigest();
|
|
5743
|
+
document.getElementById('plan-scroll').scrollTop = 0;
|
|
5744
|
+
socket.emit('aside:ask', { reqId: _planDigestReq, kind: 'plan', context: _planCtl.plan || '', question: PLAN_DIGEST_Q, userAsk: '' });
|
|
5745
|
+
};
|
|
5746
|
+
socket.on('aside:delta', ({ reqId, text }) => { if (reqId === _planDigestReq) { _planDigest.text = text; renderPlanDigest(); } });
|
|
5747
|
+
socket.on('aside:done', ({ reqId, text, error, partial }) => {
|
|
5748
|
+
if (reqId !== _planDigestReq) return;
|
|
5749
|
+
_planDigestReq = null;
|
|
5750
|
+
_planDigest.text = text || partial || _planDigest.text; _planDigest.err = text ? '' : (error || '没有拿到结果,稍后再试');
|
|
5751
|
+
_planDigest.state = 'done'; renderPlanDigest();
|
|
5752
|
+
});
|
|
5753
|
+
// 选中方案(或速览)里的一段 → 「💬 对这段提意见」:以引用格式追加到意见框
|
|
5754
|
+
let _planSel = '';
|
|
5755
|
+
document.addEventListener('selectionchange', () => {
|
|
5756
|
+
const btn = document.getElementById('plan-quote-btn');
|
|
5757
|
+
const sel = window.getSelection();
|
|
5758
|
+
const text = sel && !sel.isCollapsed ? sel.toString().trim() : '';
|
|
5759
|
+
const node = text && sel.anchorNode && (sel.anchorNode.nodeType === 1 ? sel.anchorNode : sel.anchorNode.parentElement);
|
|
5760
|
+
if (!text || !node?.closest?.('#plan-scroll') || !document.getElementById('plan-sheet').classList.contains('open')) { _planSel = ''; btn.classList.remove('show'); return; }
|
|
5761
|
+
_planSel = text;
|
|
5762
|
+
btn.classList.add('show');
|
|
5763
|
+
const r = sel.getRangeAt(0).getBoundingClientRect(), bw = btn.offsetWidth, bh = btn.offsetHeight;
|
|
5764
|
+
let top = r.top - bh - 8; if (top < 8) top = r.bottom + 8;
|
|
5765
|
+
btn.style.left = Math.min(innerWidth - bw - 8, Math.max(8, r.left + r.width / 2 - bw / 2)) + 'px';
|
|
5766
|
+
btn.style.top = top + 'px';
|
|
5767
|
+
});
|
|
5768
|
+
window.planQuoteSel = function() {
|
|
5769
|
+
if (!_planSel) return;
|
|
5770
|
+
const fb = document.getElementById('plan-sheet-fb');
|
|
5771
|
+
const quote = _planSel.split('\n').map(l => '> ' + l).join('\n');
|
|
5772
|
+
const cur = fb.value.replace(/\s+$/, '');
|
|
5773
|
+
fb.value = (cur ? cur + '\n\n' : '') + quote + '\n';
|
|
5774
|
+
window.getSelection()?.removeAllRanges();
|
|
5775
|
+
document.getElementById('plan-quote-btn').classList.remove('show');
|
|
5776
|
+
fb.dispatchEvent(new Event('input'));
|
|
5777
|
+
fb.focus(); fb.setSelectionRange(fb.value.length, fb.value.length); fb.scrollTop = fb.scrollHeight;
|
|
5778
|
+
};
|
|
5590
5779
|
window.approvePlan = function() {
|
|
5591
5780
|
if (!_planCtl) return;
|
|
5592
5781
|
ensureControl();
|
|
@@ -5599,7 +5788,7 @@
|
|
|
5599
5788
|
if (!fb) { document.getElementById('plan-sheet-fb').focus(); document.getElementById('plan-sheet-fb').placeholder = '先写下要怎么改,再点这个按钮'; return; }
|
|
5600
5789
|
ensureControl();
|
|
5601
5790
|
socket.emit('agent:respond', { id: _planCtl.id, decision: { approve: false, feedback: fb } });
|
|
5602
|
-
document.getElementById('plan-sheet-fb').value = '';
|
|
5791
|
+
document.getElementById('plan-sheet-fb').value = ''; document.getElementById('plan-sheet-fb').style.height = '';
|
|
5603
5792
|
_planCtl = null; closePlanSheet(); showThinking();
|
|
5604
5793
|
};
|
|
5605
5794
|
// ── 聊天里的方案卡片:ExitPlanMode 提交的方案、写进 ~/.claude/plans/*.md 的方案文件(原来只是个折叠的「写文件」,看不到内容)──
|
|
@@ -6384,12 +6573,12 @@
|
|
|
6384
6573
|
// 把当前输入框(文字 + 已上传的图片/文件)整体存成草稿
|
|
6385
6574
|
window.saveDraftFromInput = async function(source = 'manual') {
|
|
6386
6575
|
const text = cmdInput.value.trim();
|
|
6387
|
-
if (!text && !
|
|
6576
|
+
if (!text && !_pendingImages.length && !_pendingFile) { addStatusMessage('输入框是空的,没什么可存'); return false; }
|
|
6388
6577
|
const body = {
|
|
6389
6578
|
text,
|
|
6390
6579
|
sessionId: SESSION_ID,
|
|
6391
6580
|
source,
|
|
6392
|
-
images:
|
|
6581
|
+
images: _pendingImages.map(im => ({ path: im.path, name: '图片' })),
|
|
6393
6582
|
files: _pendingFile ? [{ path: _pendingFile.path, name: _pendingFile.name }] : [],
|
|
6394
6583
|
};
|
|
6395
6584
|
try {
|
|
@@ -6416,16 +6605,16 @@
|
|
|
6416
6605
|
window.useDraft = async function(id) {
|
|
6417
6606
|
const d = _inboxCache.drafts.find(x => x.id === id);
|
|
6418
6607
|
if (!d) return;
|
|
6419
|
-
//
|
|
6420
|
-
const extra = [...(d.images || []).slice(
|
|
6421
|
-
...(d.files || []).slice(
|
|
6608
|
+
// 消息框可挂多张图(最多 MAX_IMAGES)+ 一个文件:放不下的附件以路径写进文字里,AI 照样会读
|
|
6609
|
+
const extra = [...(d.images || []).slice(MAX_IMAGES).map(a => `另附图片 ${a.path},请用 Read 工具读取。`),
|
|
6610
|
+
...(d.files || []).slice(1).map(a => `另附文件 ${a.path}(原文件名 ${a.name}),请读取分析。`)];
|
|
6422
6611
|
cmdInput.value = (d.text || '') + (extra.length ? '\n' + extra.join('\n') : '');
|
|
6423
6612
|
cmdInput.style.height = 'auto';
|
|
6424
6613
|
cmdInput.style.height = Math.min(cmdInput.scrollHeight, 200) + 'px';
|
|
6425
6614
|
clearPendingImage();
|
|
6426
6615
|
clearPendingFile();
|
|
6427
|
-
|
|
6428
|
-
|
|
6616
|
+
for (const a of (d.images || []).slice(0, MAX_IMAGES)) setPendingImage(a.path, '');
|
|
6617
|
+
if (d.files?.length) {
|
|
6429
6618
|
_pendingFile = { path: d.files[0].path, name: d.files[0].name };
|
|
6430
6619
|
const fp = document.getElementById('file-preview');
|
|
6431
6620
|
const nameEl = document.getElementById('file-preview-name');
|
|
@@ -7838,11 +8027,14 @@
|
|
|
7838
8027
|
|
|
7839
8028
|
// ── 拍照上传 ──────────────────────────────────
|
|
7840
8029
|
window.openCamera = async function() {
|
|
7841
|
-
const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*';
|
|
8030
|
+
const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.multiple = true;
|
|
7842
8031
|
input.onchange = async () => {
|
|
7843
|
-
|
|
8032
|
+
let files = [...(input.files || [])]; if (!files.length) return;
|
|
8033
|
+
const room = MAX_IMAGES - _pendingImages.length;
|
|
8034
|
+
if (room <= 0) { addStatusMessage(`一次最多 ${MAX_IMAGES} 张图片`); return; }
|
|
8035
|
+
if (files.length > room) { addStatusMessage(`一次最多 ${MAX_IMAGES} 张图片,只加了前 ${room} 张`); files = files.slice(0, room); }
|
|
7844
8036
|
const btn = document.getElementById('btn-photo'); btn.classList.add('active'); btn.disabled = true;
|
|
7845
|
-
try { await uploadImageBlob(
|
|
8037
|
+
try { for (const f of files) await uploadImageBlob(f, f.name || 'photo.jpg'); }
|
|
7846
8038
|
catch (e) { addStatusMessage('出错: ' + e.message); }
|
|
7847
8039
|
finally { btn.classList.remove('active'); btn.disabled = false; }
|
|
7848
8040
|
}; input.click();
|
package/dist/chat.html.gz
CHANGED
|
Binary file
|
package/dist/index.html
CHANGED
|
@@ -1519,7 +1519,8 @@
|
|
|
1519
1519
|
background: '生成标题、摘要等后台小活,建议选便宜的',
|
|
1520
1520
|
ask: '「问一问」和解释,建议选便宜快速的;留空 = sonnet',
|
|
1521
1521
|
voice: '语音转成书面文字,建议选便宜的;留空 = 同问一问',
|
|
1522
|
-
vision: '
|
|
1522
|
+
vision: '配了就用:聊天框发的图片先用它转成文字再交给主模型(多花几秒,聊天里可点开核对);主模型看不了图时一定要配,如本地的 m1-vision。留空 = 图片直接发给主模型。提示用户:截图标注尽量配文字,红框 / 箭头的指向看图模型常认错',
|
|
1523
|
+
visionPro: '可选:重要截图(复杂标注的 bug 单、设计稿评审)发送时点「🔍 高精度」改用它,如网关上的 Claude 等云端多模态模型;留空 = 不显示这个开关',
|
|
1523
1524
|
};
|
|
1524
1525
|
// ── 大模型网关 ────────────────────────────────────────────
|
|
1525
1526
|
const GW_SRC = { myhi: '本页配置', env: '启动 myhi 时的环境变量(ANTHROPIC_BASE_URL)', claude: 'Claude Code 配置(~/.claude/settings.json)' };
|
|
@@ -1570,8 +1571,8 @@
|
|
|
1570
1571
|
document.getElementById('set-models-dl-tools').innerHTML = ms.filter(m => !m.fit?.main).map(opt).join('') + aliases;
|
|
1571
1572
|
document.getElementById('set-models-dl-all').innerHTML = cheapFirst.map(opt).join('') + aliases;
|
|
1572
1573
|
document.getElementById('set-models-dl-vision').innerHTML = cheapFirst.filter(m => (m.caps || []).includes('vision')).map(opt).join('') + aliases;
|
|
1573
|
-
const TOOL_SCENES = { main: 1, plan: 1, subagent: 1, vision: 1 };
|
|
1574
|
-
const DL = (k) => k === 'vision' ? 'set-models-dl-vision' : TOOL_SCENES[k] ? 'set-models-dl-tools' : 'set-models-dl-all';
|
|
1574
|
+
const TOOL_SCENES = { main: 1, plan: 1, subagent: 1, vision: 1, visionPro: 1 };
|
|
1575
|
+
const DL = (k) => k === 'vision' || k === 'visionPro' ? 'set-models-dl-vision' : TOOL_SCENES[k] ? 'set-models-dl-tools' : 'set-models-dl-all';
|
|
1575
1576
|
document.getElementById('set-models-rows').innerHTML = Object.entries(d.sceneLabels || {}).map(([k, label]) =>
|
|
1576
1577
|
`<div class="set-item inp"><div class="set-lbl">${esc(label)}<small>${esc(SCENE_HINT[k] || '')}</small></div>
|
|
1577
1578
|
<div class="set-ctl"><input class="set-inp" data-scene="${esc(k)}" list="${DL(k)}" value="${esc(d.scenes?.[k] || '')}" placeholder="默认" autocomplete="off" spellcheck="false"></div>
|
|
@@ -1580,7 +1581,7 @@
|
|
|
1580
1581
|
const byId = Object.fromEntries(ms.map(m => [m.id, m]));
|
|
1581
1582
|
const check = (inp) => {
|
|
1582
1583
|
const k = inp.dataset.scene, w = document.querySelector(`[data-warn="${k}"]`);
|
|
1583
|
-
const why = TOOL_SCENES[k] && byId[inp.value.trim()]?.fit?.[k];
|
|
1584
|
+
const why = TOOL_SCENES[k] && byId[inp.value.trim()]?.fit?.[k === 'visionPro' ? 'vision' : k];
|
|
1584
1585
|
// 看图:主任务模型本身看不了图却没配看图模型 → 提醒(图片发过去 AI 会读不出来)
|
|
1585
1586
|
const mainM = byId[(document.querySelector('[data-scene="main"]')?.value || '').trim()];
|
|
1586
1587
|
const hint = k === 'vision' && !inp.value.trim() && mainM?.caps?.length && !mainM.caps.includes('vision') ? `主任务模型 ${mainM.name} 看不了图,建议配一个,否则发图片 AI 会读不出来` : '';
|
package/dist/index.html.gz
CHANGED
|
Binary file
|