@wendongfly/myhi 1.3.212 → 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 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; }
@@ -845,9 +851,10 @@
845
851
  <button class="img-remove" onclick="guideTipClose()" title="先不管">&times;</button>
846
852
  </div>
847
853
  <div id="img-preview">
848
- <img id="img-preview-thumb" src="" alt="">
849
- <span class="img-info">已添加图片,输入文字后发送</span>
850
- <button class="img-remove" onclick="clearPendingImage()" title="移除图片">&times;</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="移除全部图片">&times;</button>
851
858
  </div>
852
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)">
853
860
  <span style="font-size:1.1rem;flex-shrink:0">📎</span>
@@ -3605,14 +3612,57 @@
3605
3612
  });
3606
3613
 
3607
3614
  // ── 待发送图片 ──────────────────────────────────
3608
- let _pendingImage = null; // { path, blobUrl }
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(); };
3609
3620
 
3610
3621
  window.clearPendingImage = function() {
3611
- if (_pendingImage?.blobUrl) URL.revokeObjectURL(_pendingImage.blobUrl);
3612
- _pendingImage = null;
3613
- document.getElementById('img-preview').classList.remove('show');
3622
+ for (const im of _pendingImages) if (im.blobUrl) URL.revokeObjectURL(im.blobUrl);
3623
+ _pendingImages = [];
3624
+ _visionPro = false;
3625
+ renderPendingImages();
3614
3626
  cmdInput.placeholder = currentSession?.mode === 'agent' ? '输入消息...' : '输入命令...';
3615
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
+ }
3616
3666
 
3617
3667
  window.cancelAgent = function() {
3618
3668
  if (currentSession?.mode === 'agent' && workState !== 'idle') {
@@ -3621,13 +3671,13 @@
3621
3671
  };
3622
3672
 
3623
3673
  function setPendingImage(path, blobUrl) {
3624
- _pendingImage = { path, blobUrl };
3625
- const preview = document.getElementById('img-preview');
3626
- const thumb = document.getElementById('img-preview-thumb');
3627
- // 从草稿取回时只剩服务器路径、没有本地 blob 预览图 —— 藏掉缩略图免得显示破图
3628
- thumb.src = blobUrl || '';
3629
- thumb.style.display = blobUrl ? '' : 'none';
3630
- preview.classList.add('show');
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();
3631
3681
  cmdInput.placeholder = '输入文字描述后发送,或直接发送分析图片...';
3632
3682
  cmdInput.focus();
3633
3683
  }
@@ -3818,7 +3868,7 @@
3818
3868
  function sendCommand() {
3819
3869
  const text = cmdInput.value.trim();
3820
3870
  if (GUIDE_REMEMBER_RE.test(text)) _guideRemember = true; // 「以后都… / 记住…」:这一轮干完建议写进项目说明
3821
- if (!text && !_pendingImage && !_pendingFile) return;
3871
+ if (!text && !_pendingImages.length && !_pendingFile) return;
3822
3872
 
3823
3873
  // AI 还在跑这一轮(可能正开着子代理,整轮 result 未到,服务端 isBusy 会拒收):
3824
3874
  // 不再把输入连同刚上传的图片一起丢掉,转存草稿,本轮干完再取回。
@@ -3839,33 +3889,27 @@
3839
3889
  }
3840
3890
  endStream();
3841
3891
 
3842
- const imgPath = _pendingImage?.path || null;
3892
+ const imgPaths = _pendingImages.map(im => im.path);
3843
3893
  const filePath = _pendingFile?.path || null;
3844
3894
  const fileName = _pendingFile?.name || '';
3845
3895
  const userText = text;
3846
- const displayText = imgPath ? `📷 ${userText || '分析图片'}`
3847
- : filePath ? `📎 ${fileName}${userText ? ' ' + userText : ''}`
3848
- : text;
3896
+ const displayText = attachDisplay(imgPaths, _pendingFile, userText);
3849
3897
 
3850
3898
  addInputMessage(displayText);
3851
3899
 
3852
3900
  if (currentSession?.mode === 'agent') {
3853
- const prompt = imgPath
3854
- ? `用户上传了一张图片,路径是 ${imgPath},请用 Read 工具读取这张图片。用户的要求是:${userText || '描述图片内容'}`
3855
- : filePath
3856
- ? `用户上传了一个临时文件用于本次分析(不在项目文件树里),路径是 ${filePath}(原文件名 ${fileName}),请用合适的工具(如 Read)读取并分析它。用户的要求是:${userText || '分析这个文件'}`
3857
- : text;
3858
- socket.emit('agent:query', { prompt });
3901
+ const prompt = attachPrompt(imgPaths, _pendingFile, userText);
3902
+ socket.emit('agent:query', { prompt, visionPro: imgPaths.length > 0 && _visionPro });
3859
3903
  // 本地 slash(/model /clear /plan /resume /rename)只回一条 info、无 result,不进 thinking 态
3860
3904
  if (!LOCAL_SLASH_RE.test(text)) showThinking();
3861
3905
  } else {
3862
- const attach = imgPath || filePath;
3906
+ const attach = [...imgPaths, filePath].filter(Boolean).join(' ');
3863
3907
  const input = attach
3864
3908
  ? (userText ? `${userText} ${attach}` : attach)
3865
3909
  : text;
3866
3910
  socket.emit('input', input + '\r');
3867
3911
  }
3868
- cmdHistory.unshift(imgPath ? `[图片] ${userText}` : filePath ? `[文件] ${fileName} ${userText}` : text);
3912
+ cmdHistory.unshift(imgPaths.length ? `[图片${imgPaths.length > 1 ? '×' + imgPaths.length : ''}] ${userText}` : filePath ? `[文件] ${fileName} ${userText}` : text);
3869
3913
  if (cmdHistory.length > 100) cmdHistory.pop();
3870
3914
  historyIdx = -1;
3871
3915
  cmdInput.value = '';
@@ -3897,17 +3941,11 @@
3897
3941
  }
3898
3942
  window.injectFromInput = function() {
3899
3943
  const text = cmdInput.value.trim();
3900
- if (!text && !_pendingImage && !_pendingFile) { addStatusMessage('先在输入框里写下要补充的话,再点 ⚡ 插话'); cmdInput.focus(); return; }
3944
+ if (!text && !_pendingImages.length && !_pendingFile) { addStatusMessage('先在输入框里写下要补充的话,再点 ⚡ 插话'); cmdInput.focus(); return; }
3901
3945
  if (!canInjectNow()) { sendCommand(); return; }
3902
- const imgPath = _pendingImage?.path || null;
3903
- const filePath = _pendingFile?.path || null;
3904
- const fileName = _pendingFile?.name || '';
3905
- const display = imgPath ? `📷 ${text || '分析图片'}` : filePath ? `📎 ${fileName}${text ? ' ' + text : ''}` : text;
3906
- const prompt = imgPath
3907
- ? `用户上传了一张图片,路径是 ${imgPath},请用 Read 工具读取这张图片。用户的要求是:${text || '描述图片内容'}`
3908
- : filePath
3909
- ? `用户上传了一个临时文件用于本次分析(不在项目文件树里),路径是 ${filePath}(原文件名 ${fileName}),请用合适的工具(如 Read)读取并分析它。用户的要求是:${text || '分析这个文件'}`
3910
- : text;
3946
+ const imgPaths = _pendingImages.map(im => im.path);
3947
+ const display = attachDisplay(imgPaths, _pendingFile, text);
3948
+ const prompt = attachPrompt(imgPaths, _pendingFile, text);
3911
3949
  injectEmit({ prompt, display }, () => {
3912
3950
  cmdInput.value = ''; cmdInput.style.height = 'auto';
3913
3951
  clearPendingImage(); clearPendingFile();
@@ -6535,12 +6573,12 @@
6535
6573
  // 把当前输入框(文字 + 已上传的图片/文件)整体存成草稿
6536
6574
  window.saveDraftFromInput = async function(source = 'manual') {
6537
6575
  const text = cmdInput.value.trim();
6538
- if (!text && !_pendingImage && !_pendingFile) { addStatusMessage('输入框是空的,没什么可存'); return false; }
6576
+ if (!text && !_pendingImages.length && !_pendingFile) { addStatusMessage('输入框是空的,没什么可存'); return false; }
6539
6577
  const body = {
6540
6578
  text,
6541
6579
  sessionId: SESSION_ID,
6542
6580
  source,
6543
- images: _pendingImage ? [{ path: _pendingImage.path, name: '图片' }] : [],
6581
+ images: _pendingImages.map(im => ({ path: im.path, name: '图片' })),
6544
6582
  files: _pendingFile ? [{ path: _pendingFile.path, name: _pendingFile.name }] : [],
6545
6583
  };
6546
6584
  try {
@@ -6567,16 +6605,16 @@
6567
6605
  window.useDraft = async function(id) {
6568
6606
  const d = _inboxCache.drafts.find(x => x.id === id);
6569
6607
  if (!d) return;
6570
- // 消息框一次只能挂一张图 / 一个文件:合并来的多余附件以路径写进文字里,AI 照样会读
6571
- const extra = [...(d.images || []).slice(1).map(a => `另附图片 ${a.path},请用 Read 工具读取。`),
6572
- ...(d.files || []).slice(d.images?.length ? 0 : 1).map(a => `另附文件 ${a.path}(原文件名 ${a.name}),请读取分析。`)];
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}),请读取分析。`)];
6573
6611
  cmdInput.value = (d.text || '') + (extra.length ? '\n' + extra.join('\n') : '');
6574
6612
  cmdInput.style.height = 'auto';
6575
6613
  cmdInput.style.height = Math.min(cmdInput.scrollHeight, 200) + 'px';
6576
6614
  clearPendingImage();
6577
6615
  clearPendingFile();
6578
- if (d.images?.length) setPendingImage(d.images[0].path, '');
6579
- else if (d.files?.length) {
6616
+ for (const a of (d.images || []).slice(0, MAX_IMAGES)) setPendingImage(a.path, '');
6617
+ if (d.files?.length) {
6580
6618
  _pendingFile = { path: d.files[0].path, name: d.files[0].name };
6581
6619
  const fp = document.getElementById('file-preview');
6582
6620
  const nameEl = document.getElementById('file-preview-name');
@@ -7989,11 +8027,14 @@
7989
8027
 
7990
8028
  // ── 拍照上传 ──────────────────────────────────
7991
8029
  window.openCamera = async function() {
7992
- 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;
7993
8031
  input.onchange = async () => {
7994
- const file = input.files?.[0]; if (!file) return;
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); }
7995
8036
  const btn = document.getElementById('btn-photo'); btn.classList.add('active'); btn.disabled = true;
7996
- try { await uploadImageBlob(file, 'photo.jpg'); }
8037
+ try { for (const f of files) await uploadImageBlob(f, f.name || 'photo.jpg'); }
7997
8038
  catch (e) { addStatusMessage('出错: ' + e.message); }
7998
8039
  finally { btn.classList.remove('active'); btn.disabled = false; }
7999
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 会读不出来` : '';
Binary file