@wendongfly/myhi 1.3.206 → 1.3.209

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
@@ -2573,7 +2573,22 @@
2573
2573
  return null;
2574
2574
  }
2575
2575
 
2576
+ // 看图转文字的结果:折叠块,点开能看到主模型实际拿到的图片描述(描述可能漏细节,给用户核对)
2577
+ function visionEl(msg) {
2578
+ const d = document.createElement('details');
2579
+ d.className = 'msg msg-vision';
2580
+ d.style.cssText = 'margin:2px auto 6px;max-width:92%;font-size:0.8rem;color:var(--muted);border:1px solid var(--border);border-radius:8px;padding:4px 10px';
2581
+ d.innerHTML = `<summary style="cursor:pointer">🖼 看图模型 ${escHtml(msg.vision.model || '')} 转成的文字(点开核对)</summary>` +
2582
+ (msg.vision.items || []).map(it => `<div style="margin:6px 0 2px;word-break:break-all">${escHtml(it.path || '')}</div>` +
2583
+ `<pre style="white-space:pre-wrap;word-break:break-word;margin:0;font-size:0.78rem;color:var(--text)">${escHtml(it.text || ('识别失败:' + (it.error || '')))}</pre>`).join('');
2584
+ return d;
2585
+ }
2586
+
2576
2587
  function renderHistoryMessage(msg, parent) {
2588
+ if (msg.type === 'system' && msg.vision) {
2589
+ const st = document.createElement('div'); st.className = 'msg msg-status'; st.textContent = '— ' + msg.message + ' —';
2590
+ parent.appendChild(st); parent.appendChild(visionEl(msg)); return;
2591
+ }
2577
2592
  if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
2578
2593
  for (const b of msg.message.content) { const k = blockKey(b, msg.message.id); if (k) _replayedBlocks.add(k); }
2579
2594
  } else if (msg.type === 'tool_result' && msg.id) _replayedBlocks.add('tr:' + msg.id);
@@ -2606,11 +2621,21 @@
2606
2621
  const sel = buildSyntheticUserEl(msg.content);
2607
2622
  if (sel) { parent.appendChild(sel); return; }
2608
2623
  if (syntheticUserKind(msg.content)) return;
2624
+ // 看图转文字后发给模型的消息(对话记录里就是改写后的全文):气泡只显示原话,图片描述收进折叠块
2625
+ let content = msg.content, vis = null;
2626
+ const vi = typeof content === 'string' ? content.indexOf('\n\n(注意:你当前用的模型不能直接看图') : -1;
2627
+ if (vi > 0) {
2628
+ const tail = content.slice(vi);
2629
+ vis = { message: '', vision: { model: (tail.match(/看图模型 (\S+) 把图片/) || [])[1] || '',
2630
+ items: [...tail.matchAll(/【图片 ([^】]+)】\n([\s\S]*?)(?=\n\n【图片 |$)/g)].map(m => ({ path: m[1], text: m[2] })) } };
2631
+ content = content.slice(0, vi);
2632
+ }
2609
2633
  const el = document.createElement('div');
2610
2634
  el.className = 'msg msg-input' + (msg.inject ? ' msg-inject' : '');
2611
2635
  const full = msg.timestamp ? new Date(msg.timestamp).toLocaleString('zh-CN') : '';
2612
- el.innerHTML = `<div><div class="bubble">${escHtml(msg.content)}</div><div class="meta" title="${escHtml(full)}">${injectTagHtml(msg.inject)}${msg.timestamp ? formatTime(new Date(msg.timestamp)) : ''}</div></div>`;
2636
+ el.innerHTML = `<div><div class="bubble">${escHtml(content)}</div><div class="meta" title="${escHtml(full)}">${injectTagHtml(msg.inject)}${msg.timestamp ? formatTime(new Date(msg.timestamp)) : ''}</div></div>`;
2613
2637
  parent.appendChild(el);
2638
+ if (vis) parent.appendChild(visionEl(vis));
2614
2639
  return;
2615
2640
  }
2616
2641
 
@@ -3221,6 +3246,7 @@
3221
3246
  _ctxNudgeLevel = 0; _lastCtxTokens = 0; ctxUpdate(0);
3222
3247
  }
3223
3248
  addStatusMessage(msg.message);
3249
+ if (msg.vision) { chatArea.appendChild(visionEl(msg)); scrollToBottom(); }
3224
3250
  // 同步计划模式状态到按钮
3225
3251
  if (msg.message.includes('进入计划模式')) {
3226
3252
  if (currentSession) currentSession.permissionMode = 'plan';
@@ -5630,10 +5656,12 @@
5630
5656
  await loadModelInfo();
5631
5657
  const cur = currentSession?.modelId || '';
5632
5658
  const main = _modelInfo?.scenes?.main || '';
5659
+ // 计划模式且系统设置里配了「计划」模型 → 这段时间实际用它(批准执行后回到主任务模型)
5660
+ const plan = currentSession?.permissionMode === 'plan' ? (_modelInfo?.scenes?.plan || '') : '';
5633
5661
  const el = document.getElementById('model-chip-name');
5634
- el.textContent = cur ? modelName(cur) : main ? modelName(main) : '默认';
5662
+ el.textContent = plan ? '计划·' + modelName(plan) : cur ? modelName(cur) : main ? modelName(main) : '默认';
5635
5663
  const chip = document.getElementById('model-chip'), q = quotaState();
5636
- chip.title = '切换模型 · 当前:' + (cur || (main ? main + '(主会话默认)' : 'Claude Code 默认')) + (q.text ? '\n' + (q.out ? q.msg : q.text) : '');
5664
+ chip.title = '切换模型 · 当前:' + (plan ? plan + '(计划模式,系统设置 · 计划)\n执行时:' : '') + (cur || (main ? main + '(主任务默认)' : 'Claude Code 默认')) + (q.text ? '\n' + (q.out ? q.msg : q.text) : '');
5637
5665
  chip.style.borderColor = q.out ? 'var(--red)' : ''; // 积分用尽:按钮描红
5638
5666
  };
5639
5667
  window.openModelPop = async function() {
@@ -5648,7 +5676,7 @@
5648
5676
  const row = (id, name, sub, right, tags, on, unfit, tip) => `<div class="mp-i${on ? ' on' : ''}" data-model="${escAttr(id)}" data-label="${escAttr(name)}"${unfit ? ` data-unfit="${escAttr(unfit)}" style="opacity:0.55"` : ''}${tip ? ` title="${escAttr(tip)}"` : ''}><span class="mp-n">${escHtml(name)}${unfit ? `<span class="mp-sub" style="color:var(--yellow)">⚠ ${escHtml(unfit)}</span>` : sub ? `<span class="mp-sub">${escHtml(sub)}</span>` : ''}</span>${(tags || []).map(t => TAG_LABEL[t] ? `<span class="mp-tag" style="color:${TAG_LABEL[t][1]};border-color:${TAG_LABEL[t][1]}">${TAG_LABEL[t][0]}</span>` : '').join('')}${right ? `<span class="mp-r">${escHtml(right)}</span>` : ''}${on ? '<span class="mp-ck">✓</span>' : ''}</div>`;
5649
5677
  const q = quotaState();
5650
5678
  let html = q.text ? `<div class="mp-h" style="${q.out ? 'color:var(--red);font-weight:600' : ''}">${escHtml(q.out ? q.msg : q.text)}</div>` : '';
5651
- html += row('default', '默认', main ? '主会话默认:' + main : '跟随 Claude Code 默认', '', [], !cur);
5679
+ html += row('default', '默认', main ? '主任务默认:' + main : '跟随 Claude Code 默认', '', [], !cur);
5652
5680
  const gm = (d.models || []).filter(m => !m.id.includes('*')); // 通配别名(claude-*)不能直接选
5653
5681
  const listed = new Set(['default']);
5654
5682
  if (gm.length) {
@@ -5661,17 +5689,22 @@
5661
5689
  const sub = [m.kind === 'alias' && m.target ? '→ ' + m.target : '', m.caps.map(c => CAP_ICON[c]).filter(Boolean).join(' ')].filter(Boolean).join(' · ');
5662
5690
  // 右侧:有渠道系数就显示倍率(如 0.51x),否则显示单价;单价放悬停提示里
5663
5691
  const price = m.inputCost != null ? `$${m.inputCost}/$${m.outputCost} 每百万 token` : '';
5664
- const right = m.multiplier != null ? `${m.multiplier}x` : (m.inputCost != null ? `$${m.inputCost}/$${m.outputCost}` : '');
5665
- html += row(m.id, m.kind === 'alias' ? m.id : m.name, sub, right, m.tags, cur === m.id, m.fit?.main || '', [price, m.multiplier != null ? `渠道系数 ${m.multiplier}` : ''].filter(Boolean).join(' · '));
5692
+ // 时段价此刻生效(如「闲时 0.5x」)→ 放在右侧倍率前面
5693
+ const tn = m.timeNow ? `${m.timeNow.label}${m.timeNow.multiplier != null ? ' ' + m.timeNow.multiplier + 'x' : ''}` : '';
5694
+ const right = [tn, m.multiplier != null ? `${m.multiplier}x` : (m.inputCost != null ? `$${m.inputCost}/$${m.outputCost}` : '')].filter(Boolean).join(' · ');
5695
+ html += row(m.id, m.kind === 'alias' ? m.id : m.name, sub, right, m.tags, cur === m.id, m.fit?.main || '', [price, m.multiplier != null ? `渠道系数 ${m.multiplier}` : '', tn ? '当前时段:' + tn : ''].filter(Boolean).join(' · '));
5666
5696
  }
5667
5697
  }
5668
5698
  } else if (d.gateway && d.error) html += `<div class="mp-h">网关模型列表读取失败:${escHtml(d.error)}</div>`;
5699
+ // 没有自定义网关(Claude 原生):和原来一样列 Claude 模型;有网关:只列网关模型 + 下方手填自定义
5700
+ const claudeMs = d.gateway ? [] : MODELS.filter(x => x.id !== 'default');
5701
+ claudeMs.forEach(m => listed.add(m.id));
5669
5702
  // 自定义:本地场景配置里填过、或本会话正在用、但不在网关列表里的名字
5670
5703
  const custom = [...new Set([cur, ...Object.values(d.scenes || {})].filter(x => x && !listed.has(x)))];
5671
5704
  if (custom.length) { html += '<div class="mp-h">自定义模型</div>'; for (const id of custom) { listed.add(id); html += row(id, id, '', '', [], cur === id); } }
5672
- html += `<div class="mp-h">Claude 别名${d.gateway ? ' · 经网关映射' : ''}</div>`;
5673
- for (const m of MODELS.filter(x => x.id !== 'default' && !listed.has(x.id))) html += row(m.id, m.label, m.desc, '', [], cur === m.id);
5705
+ if (claudeMs.length) { html += '<div class="mp-h">Claude 模型</div>'; for (const m of claudeMs) html += row(m.id, m.label, m.desc, '', [], cur === m.id); }
5674
5706
  list.innerHTML = html;
5707
+ document.getElementById('model-pop-custom').style.display = d.gateway ? '' : 'none';
5675
5708
  list.querySelectorAll('[data-model]').forEach(el => el.onclick = () => {
5676
5709
  if (el.dataset.unfit && !confirm(`「${el.dataset.label}」${el.dataset.unfit}。\n\n用它当主会话模型,AI 很可能干不了活。仍要切换吗?`)) return;
5677
5710
  closeModelPop(); selectModel({ id: el.dataset.model, label: el.dataset.label });
@@ -6631,6 +6664,7 @@
6631
6664
  btn.textContent = isPlan ? '退出计划' : '计划';
6632
6665
  btn.style.color = isPlan ? 'var(--orange)' : '';
6633
6666
  btn.style.fontWeight = isPlan ? '600' : '';
6667
+ if (currentSession?.mode === 'agent' && window.refreshModelChip) refreshModelChip(); // 计划模型可能不同
6634
6668
  }
6635
6669
  window.togglePlanMode = function() {
6636
6670
  const isPlan = currentSession?.permissionMode === 'plan';
package/dist/chat.html.gz CHANGED
Binary file
package/dist/index.html CHANGED
@@ -956,16 +956,30 @@
956
956
  <button class="set-link" onclick="closeSettings();showUsage()">📊 用量 <small>费用 / token 统计</small></button>
957
957
  </div>
958
958
 
959
+ <!-- 大模型网关(仅管理员;单用户模式的主人就是管理员,不用再去 /admin)-->
960
+ <div id="set-gw" class="admin-only">
961
+ <div class="set-group-t">系统设置 · 大模型网关</div>
962
+ <div class="set-card">
963
+ <div class="set-item" style="display:block;font-size:0.74rem;color:var(--muted);line-height:1.6" id="set-gw-src">读取中…</div>
964
+ <div class="set-item inp"><div class="set-lbl">网关地址<small>留空 = 自动用这台电脑已有的配置(环境变量 / Claude Code 配置)</small></div>
965
+ <div class="set-ctl"><input class="set-inp" id="set-gw-url" placeholder="https://gateway.example.com" autocomplete="off" spellcheck="false"></div></div>
966
+ <div class="set-item inp"><div class="set-lbl">Token / API Key</div>
967
+ <div class="set-ctl"><input class="set-inp" id="set-gw-key" type="password" placeholder="Token / API Key" autocomplete="new-password"></div></div>
968
+ <div class="set-item" style="justify-content:flex-end"><button class="set-btn" id="set-gw-save" onclick="saveGatewayCfg()">保存</button></div>
969
+ </div>
970
+ <div id="set-gw-status" class="set-status"></div>
971
+ </div>
972
+
959
973
  <!-- 场景模型:同一个网关 + token,不同场景用不同模型(仅管理员)-->
960
974
  <div id="set-models" class="admin-only">
961
- <div class="set-group-t">模型 · 所有任务生效</div>
975
+ <div class="set-group-t">系统设置 · 模型(所有任务生效)</div>
962
976
  <div class="set-card">
963
977
  <div class="set-item" style="display:block;font-size:0.74rem;color:var(--muted);line-height:1.6" id="set-models-src">读取网关模型列表…</div>
964
978
  <div id="set-models-rows"></div>
965
979
  <div class="set-item" style="justify-content:flex-end"><button class="set-btn" id="set-models-save" onclick="saveSceneModels()">保存</button></div>
966
980
  </div>
967
981
  <div id="set-models-status" class="set-status"></div>
968
- <datalist id="set-models-dl-tools"></datalist>
982
+ <datalist id="set-models-dl-tools"></datalist><datalist id="set-models-dl-vision"></datalist>
969
983
  <datalist id="set-models-dl-all"></datalist>
970
984
  </div>
971
985
 
@@ -1329,7 +1343,7 @@
1329
1343
  window.openSettings = function() {
1330
1344
  renderAppearance();
1331
1345
  ['set-machine-status', 'set-asr-status', 'set-restart-status', 'set-up-status', 'set-proto-status'].filter(id => document.getElementById(id)).forEach(id => setStatus(id, ''));
1332
- if (_isAdmin) { loadProto(); loadRegistry(); loadSceneModels(); }
1346
+ if (_isAdmin) { loadProto(); loadRegistry(); loadGatewayCfg(); loadSceneModels(); }
1333
1347
  document.querySelectorAll('#set-sheet .set-btn').forEach(b => b.classList.remove('dirty'));
1334
1348
  document.getElementById('set-backdrop').classList.add('open');
1335
1349
  document.getElementById('set-sheet').classList.add('open');
@@ -1497,12 +1511,38 @@
1497
1511
  // ── 场景模型 ────────────────────────────────────────────
1498
1512
  // 每个场景一个输入框:可从网关模型列表里选(datalist),也可手填任意模型名 / 别名;留空 = 用默认
1499
1513
  const SCENE_HINT = {
1500
- main: '会话里没手动选模型时用它;留空 = Claude Code 默认',
1501
- subagent: 'AI 把任务拆成子任务时用;留空 = 跟主模型一样',
1514
+ main: '任务对话默认用它(聊天框里没单独选模型时);留空 = Claude Code 默认',
1515
+ plan: '计划模式(只调研、出方案)用,可选更强的;批准执行后回到主任务模型;留空 = 跟主任务一样',
1516
+ subagent: 'AI 把任务拆成子任务时用;留空 = 跟主任务一样',
1502
1517
  background: '生成标题、摘要等后台小活,建议选便宜的',
1503
1518
  ask: '「问一问」和解释,建议选便宜快速的;留空 = sonnet',
1504
1519
  voice: '语音转成书面文字,建议选便宜的;留空 = 同问一问',
1520
+ vision: '主任务模型看不了图时,先用它把聊天框发的图片转成文字再交给主模型(多花几秒,可在聊天里点开核对);留空 = 不转',
1505
1521
  };
1522
+ // ── 大模型网关 ────────────────────────────────────────────
1523
+ const GW_SRC = { myhi: '本页配置', env: '启动 myhi 时的环境变量(ANTHROPIC_BASE_URL)', claude: 'Claude Code 配置(~/.claude/settings.json)' };
1524
+ async function loadGatewayCfg() {
1525
+ let d; try { d = await (await fetch('/api/gateway')).json(); } catch { return; }
1526
+ const e = d.effective;
1527
+ document.getElementById('set-gw-src').textContent = !e
1528
+ ? '还没有自定义网关:AI 直连 Anthropic(或用 claude 登录的账号),不提供模型选择。填上网关地址和 Key 后,新建任务走网关,并可按场景从网关模型里选。'
1529
+ : e.source === 'myhi' ? `当前网关:${e.baseUrl}(Key ${e.apiKey})`
1530
+ : `当前网关:${e.baseUrl}(Key ${e.apiKey})——自动读取自 ${GW_SRC[e.source] || e.source}。要换成别的,在下面填写即可(这里填的优先)。`;
1531
+ document.getElementById('set-gw-url').value = d.baseUrl || '';
1532
+ document.getElementById('set-gw-key').value = '';
1533
+ document.getElementById('set-gw-key').placeholder = d.hasKey ? `已设置(${d.apiKey}),留空保持不变` : 'Token / API Key';
1534
+ const btn = document.getElementById('set-gw-save');
1535
+ btn.classList.remove('dirty');
1536
+ ['set-gw-url', 'set-gw-key'].forEach(id => document.getElementById(id).oninput = () => btn.classList.add('dirty'));
1537
+ }
1538
+ window.saveGatewayCfg = async function() {
1539
+ const baseUrl = document.getElementById('set-gw-url').value.trim(), apiKey = document.getElementById('set-gw-key').value.trim();
1540
+ if (baseUrl && !/^https?:\/\//i.test(baseUrl)) { setStatus('set-gw-status', '网关地址要以 http:// 或 https:// 开头'); return; }
1541
+ let r; try { r = await (await fetch('/api/gateway', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ baseUrl, apiKey }) })).json(); } catch (e) { r = { error: e.message }; }
1542
+ setStatus('set-gw-status', r.ok ? '已保存:新建的任务走新网关;正在跑的任务下次重启 AI 后生效' : '保存失败:' + (r.error || ''));
1543
+ if (r.ok) { await loadGatewayCfg(); fetch('/api/models?refresh=1').catch(() => {}).finally(loadSceneModels); }
1544
+ };
1545
+
1506
1546
  async function loadSceneModels() {
1507
1547
  let d; try { d = await (await fetch('/api/models')).json(); } catch { return; }
1508
1548
  if (!d.ok) return;
@@ -1510,26 +1550,35 @@
1510
1550
  const ms = (d.models || []).filter(m => !m.id.includes('*'));
1511
1551
  const t = d.token, quota = t && (t.dailyLimit != null || t.monthlyLimit != null)
1512
1552
  ? ` 积分:${t.dailyLimit != null ? `今日已用 ${t.dailyUsed ?? '?'} / ${t.dailyLimit}` : ''}${t.monthlyLimit != null ? ` 本月已用 ${t.monthlyUsed ?? '?'} / ${t.monthlyLimit}` : ''}。` : '';
1513
- src.textContent = !d.gateway ? '没有配置大模型网关:可填 Claude 别名(sonnet / opus / haiku)或完整模型名。'
1514
- : d.error ? '网关模型列表读取失败(' + d.error + '),可以手填模型名。'
1553
+ // 没配自定义网关:不提供模型选择(全部跟随 Claude Code 默认),整组只留一句说明
1554
+ const noGw = !d.gateway;
1555
+ document.getElementById('set-models-rows').style.display = noGw ? 'none' : '';
1556
+ document.getElementById('set-models-save').parentElement.style.display = noGw ? 'none' : '';
1557
+ if (noGw) { src.textContent = '没有配置自定义网关:所有任务使用 Claude Code 默认模型,不需要选模型。在上面「大模型网关」里配置后,可在这里按场景(主任务 / 计划 / 问一问 / 看图…)从网关模型里选。'; return; }
1558
+ src.textContent = d.error ? '网关模型列表读取失败(' + d.error + '),可以手填模型名。'
1515
1559
  : `网关上有 ${ms.length} 个模型${d.scoped ? '(这个 token 可用的)' : '(网关是旧版,未按 token 过滤,个别模型可能用不了)'},点输入框可选,也可以手填。` + quota;
1516
1560
  // 两份候选:主会话 / 子代理只列能用工具干活的;问一问、语音整理、后台小任务全列,便宜的排前面
1517
1561
  const opt = (m) => `<option value="${esc(m.id)}">${esc([m.name !== m.id ? m.name : '', m.kind === 'alias' && m.target ? '→ ' + m.target : '', (m.tags || []).includes('cheap') ? '便宜' : '', m.multiplier != null ? m.multiplier + 'x' : '', (m.caps || []).includes('vision') ? '能看图' : ''].filter(Boolean).join(' · '))}</option>`;
1518
- const aliases = ['sonnet', 'opus', 'haiku'].map(a => `<option value="${a}">Claude 别名</option>`).join('');
1562
+ const aliases = ''; // 模型只从网关目录选(不再列 Claude 别名)
1519
1563
  const cheapFirst = [...ms].sort((a, b) => ((b.tags || []).includes('cheap') - (a.tags || []).includes('cheap')));
1520
1564
  document.getElementById('set-models-dl-tools').innerHTML = ms.filter(m => !m.fit?.main).map(opt).join('') + aliases;
1521
1565
  document.getElementById('set-models-dl-all').innerHTML = cheapFirst.map(opt).join('') + aliases;
1522
- const TOOL_SCENES = { main: 1, subagent: 1 };
1566
+ document.getElementById('set-models-dl-vision').innerHTML = cheapFirst.filter(m => (m.caps || []).includes('vision')).map(opt).join('') + aliases;
1567
+ const TOOL_SCENES = { main: 1, plan: 1, subagent: 1, vision: 1 };
1568
+ const DL = (k) => k === 'vision' ? 'set-models-dl-vision' : TOOL_SCENES[k] ? 'set-models-dl-tools' : 'set-models-dl-all';
1523
1569
  document.getElementById('set-models-rows').innerHTML = Object.entries(d.sceneLabels || {}).map(([k, label]) =>
1524
1570
  `<div class="set-item inp"><div class="set-lbl">${esc(label)}<small>${esc(SCENE_HINT[k] || '')}</small></div>
1525
- <div class="set-ctl"><input class="set-inp" data-scene="${esc(k)}" list="${TOOL_SCENES[k] ? 'set-models-dl-tools' : 'set-models-dl-all'}" value="${esc(d.scenes?.[k] || '')}" placeholder="默认" autocomplete="off" spellcheck="false"></div>
1571
+ <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>
1526
1572
  <div class="set-warn" data-warn="${esc(k)}" style="display:none;font-size:0.72rem;color:var(--yellow,#d29922)"></div></div>`).join('');
1527
1573
  // 填了不适合这个场景的模型(不支持工具等)→ 输入框下提示原因(不硬拦:网关元数据可能没填全)
1528
1574
  const byId = Object.fromEntries(ms.map(m => [m.id, m]));
1529
1575
  const check = (inp) => {
1530
1576
  const k = inp.dataset.scene, w = document.querySelector(`[data-warn="${k}"]`);
1531
1577
  const why = TOOL_SCENES[k] && byId[inp.value.trim()]?.fit?.[k];
1532
- w.textContent = why ? '⚠ ' + why : ''; w.style.display = why ? '' : 'none';
1578
+ // 看图:主任务模型本身看不了图却没配看图模型 → 提醒(图片发过去 AI 会读不出来)
1579
+ const mainM = byId[(document.querySelector('[data-scene="main"]')?.value || '').trim()];
1580
+ const hint = k === 'vision' && !inp.value.trim() && mainM?.caps?.length && !mainM.caps.includes('vision') ? `主任务模型 ${mainM.name} 看不了图,建议配一个,否则发图片 AI 会读不出来` : '';
1581
+ w.textContent = why ? '⚠ ' + why : hint ? '⚠ ' + hint : ''; w.style.display = why || hint ? '' : 'none';
1533
1582
  };
1534
1583
  document.querySelectorAll('#set-models-rows [data-scene]').forEach(i => { check(i); i.addEventListener('input', () => check(i)); });
1535
1584
  // 和其他设置项一样:改了才出现「保存」
Binary file