@wendongfly/myhi 1.3.206 → 1.3.208

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
@@ -1021,7 +1021,6 @@
1021
1021
  <div id="model-pop">
1022
1022
  <div id="model-pop-list"></div>
1023
1023
  <div class="mp-foot">
1024
- <input id="model-pop-custom" placeholder="其他模型:输入名字后回车" autocomplete="off" spellcheck="false" onkeydown="if(event.key==='Enter'&&this.value.trim()){closeModelPop();selectModel({id:this.value.trim(),label:this.value.trim()});this.value=''}">
1025
1024
  <a id="model-pop-cfg" href="/?settings=1" target="_top">⚙ 配置模型</a>
1026
1025
  </div>
1027
1026
  </div>
@@ -2573,7 +2572,22 @@
2573
2572
  return null;
2574
2573
  }
2575
2574
 
2575
+ // 看图转文字的结果:折叠块,点开能看到主模型实际拿到的图片描述(描述可能漏细节,给用户核对)
2576
+ function visionEl(msg) {
2577
+ const d = document.createElement('details');
2578
+ d.className = 'msg msg-vision';
2579
+ 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';
2580
+ d.innerHTML = `<summary style="cursor:pointer">🖼 看图模型 ${escHtml(msg.vision.model || '')} 转成的文字(点开核对)</summary>` +
2581
+ (msg.vision.items || []).map(it => `<div style="margin:6px 0 2px;word-break:break-all">${escHtml(it.path || '')}</div>` +
2582
+ `<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('');
2583
+ return d;
2584
+ }
2585
+
2576
2586
  function renderHistoryMessage(msg, parent) {
2587
+ if (msg.type === 'system' && msg.vision) {
2588
+ const st = document.createElement('div'); st.className = 'msg msg-status'; st.textContent = '— ' + msg.message + ' —';
2589
+ parent.appendChild(st); parent.appendChild(visionEl(msg)); return;
2590
+ }
2577
2591
  if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
2578
2592
  for (const b of msg.message.content) { const k = blockKey(b, msg.message.id); if (k) _replayedBlocks.add(k); }
2579
2593
  } else if (msg.type === 'tool_result' && msg.id) _replayedBlocks.add('tr:' + msg.id);
@@ -2606,11 +2620,21 @@
2606
2620
  const sel = buildSyntheticUserEl(msg.content);
2607
2621
  if (sel) { parent.appendChild(sel); return; }
2608
2622
  if (syntheticUserKind(msg.content)) return;
2623
+ // 看图转文字后发给模型的消息(对话记录里就是改写后的全文):气泡只显示原话,图片描述收进折叠块
2624
+ let content = msg.content, vis = null;
2625
+ const vi = typeof content === 'string' ? content.indexOf('\n\n(注意:你当前用的模型不能直接看图') : -1;
2626
+ if (vi > 0) {
2627
+ const tail = content.slice(vi);
2628
+ vis = { message: '', vision: { model: (tail.match(/看图模型 (\S+) 把图片/) || [])[1] || '',
2629
+ items: [...tail.matchAll(/【图片 ([^】]+)】\n([\s\S]*?)(?=\n\n【图片 |$)/g)].map(m => ({ path: m[1], text: m[2] })) } };
2630
+ content = content.slice(0, vi);
2631
+ }
2609
2632
  const el = document.createElement('div');
2610
2633
  el.className = 'msg msg-input' + (msg.inject ? ' msg-inject' : '');
2611
2634
  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>`;
2635
+ 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
2636
  parent.appendChild(el);
2637
+ if (vis) parent.appendChild(visionEl(vis));
2614
2638
  return;
2615
2639
  }
2616
2640
 
@@ -3150,7 +3174,8 @@
3150
3174
  updateControlUI(session.controlHolder === socket.id, session.controlHolderName);
3151
3175
 
3152
3176
  claudeSession = isClaudeSession(session) || session.mode === 'agent';
3153
- document.getElementById('model-chip').style.display = session.mode === 'agent' ? '' : 'none';
3177
+ // 模型按钮只在配了自定义网关时出现(列表来自网关),由 refreshModelChip 决定显隐
3178
+ if (session.mode !== 'agent') document.getElementById('model-chip').style.display = 'none';
3154
3179
  if (session.mode === 'agent') refreshModelChip();
3155
3180
  updateShortcutBar();
3156
3181
  updatePlanButton();
@@ -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,14 @@
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
+ // 没配自定义网关(直连 Anthropic 等):不提供模型选择,跟随 Claude Code 默认
5665
+ chip.style.display = currentSession?.mode === 'agent' && _modelInfo?.gateway ? '' : 'none';
5666
+ chip.title = '切换模型 · 当前:' + (plan ? plan + '(计划模式,系统设置 · 计划)\n执行时:' : '') + (cur || (main ? main + '(主任务默认)' : 'Claude Code 默认')) + (q.text ? '\n' + (q.out ? q.msg : q.text) : '');
5637
5667
  chip.style.borderColor = q.out ? 'var(--red)' : ''; // 积分用尽:按钮描红
5638
5668
  };
5639
5669
  window.openModelPop = async function() {
@@ -5648,7 +5678,7 @@
5648
5678
  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
5679
  const q = quotaState();
5650
5680
  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);
5681
+ html += row('default', '默认', main ? '主任务默认:' + main : '跟随 Claude Code 默认', '', [], !cur);
5652
5682
  const gm = (d.models || []).filter(m => !m.id.includes('*')); // 通配别名(claude-*)不能直接选
5653
5683
  const listed = new Set(['default']);
5654
5684
  if (gm.length) {
@@ -5661,16 +5691,18 @@
5661
5691
  const sub = [m.kind === 'alias' && m.target ? '→ ' + m.target : '', m.caps.map(c => CAP_ICON[c]).filter(Boolean).join(' ')].filter(Boolean).join(' · ');
5662
5692
  // 右侧:有渠道系数就显示倍率(如 0.51x),否则显示单价;单价放悬停提示里
5663
5693
  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(' · '));
5694
+ // 时段价此刻生效(如「闲时 0.5x」)→ 放在右侧倍率前面
5695
+ const tn = m.timeNow ? `${m.timeNow.label}${m.timeNow.multiplier != null ? ' ' + m.timeNow.multiplier + 'x' : ''}` : '';
5696
+ const right = [tn, m.multiplier != null ? `${m.multiplier}x` : (m.inputCost != null ? `$${m.inputCost}/$${m.outputCost}` : '')].filter(Boolean).join(' · ');
5697
+ 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
5698
  }
5667
5699
  }
5668
5700
  } else if (d.gateway && d.error) html += `<div class="mp-h">网关模型列表读取失败:${escHtml(d.error)}</div>`;
5669
5701
  // 自定义:本地场景配置里填过、或本会话正在用、但不在网关列表里的名字
5670
5702
  const custom = [...new Set([cur, ...Object.values(d.scenes || {})].filter(x => x && !listed.has(x)))];
5671
5703
  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);
5704
+ // 模型只从网关目录来;没配自定义网关就不提供选择
5705
+ if (!d.gateway) html = '<div class="mp-h" style="white-space:normal;line-height:1.5">没有配置自定义网关,使用 Claude Code 默认模型。<br>配置网关后,这里会列出网关上这个 token 可用的模型。</div>';
5674
5706
  list.innerHTML = html;
5675
5707
  list.querySelectorAll('[data-model]').forEach(el => el.onclick = () => {
5676
5708
  if (el.dataset.unfit && !confirm(`「${el.dataset.label}」${el.dataset.unfit}。\n\n用它当主会话模型,AI 很可能干不了活。仍要切换吗?`)) return;
@@ -6631,6 +6663,7 @@
6631
6663
  btn.textContent = isPlan ? '退出计划' : '计划';
6632
6664
  btn.style.color = isPlan ? 'var(--orange)' : '';
6633
6665
  btn.style.fontWeight = isPlan ? '600' : '';
6666
+ if (currentSession?.mode === 'agent' && window.refreshModelChip) refreshModelChip(); // 计划模型可能不同
6634
6667
  }
6635
6668
  window.togglePlanMode = function() {
6636
6669
  const isPlan = currentSession?.permissionMode === 'plan';
package/dist/chat.html.gz CHANGED
Binary file
package/dist/index.html CHANGED
@@ -958,14 +958,14 @@
958
958
 
959
959
  <!-- 场景模型:同一个网关 + token,不同场景用不同模型(仅管理员)-->
960
960
  <div id="set-models" class="admin-only">
961
- <div class="set-group-t">模型 · 所有任务生效</div>
961
+ <div class="set-group-t">系统设置 · 模型(所有任务生效)</div>
962
962
  <div class="set-card">
963
963
  <div class="set-item" style="display:block;font-size:0.74rem;color:var(--muted);line-height:1.6" id="set-models-src">读取网关模型列表…</div>
964
964
  <div id="set-models-rows"></div>
965
965
  <div class="set-item" style="justify-content:flex-end"><button class="set-btn" id="set-models-save" onclick="saveSceneModels()">保存</button></div>
966
966
  </div>
967
967
  <div id="set-models-status" class="set-status"></div>
968
- <datalist id="set-models-dl-tools"></datalist>
968
+ <datalist id="set-models-dl-tools"></datalist><datalist id="set-models-dl-vision"></datalist>
969
969
  <datalist id="set-models-dl-all"></datalist>
970
970
  </div>
971
971
 
@@ -1497,11 +1497,13 @@
1497
1497
  // ── 场景模型 ────────────────────────────────────────────
1498
1498
  // 每个场景一个输入框:可从网关模型列表里选(datalist),也可手填任意模型名 / 别名;留空 = 用默认
1499
1499
  const SCENE_HINT = {
1500
- main: '会话里没手动选模型时用它;留空 = Claude Code 默认',
1501
- subagent: 'AI 把任务拆成子任务时用;留空 = 跟主模型一样',
1500
+ main: '任务对话默认用它(聊天框里没单独选模型时);留空 = Claude Code 默认',
1501
+ plan: '计划模式(只调研、出方案)用,可选更强的;批准执行后回到主任务模型;留空 = 跟主任务一样',
1502
+ subagent: 'AI 把任务拆成子任务时用;留空 = 跟主任务一样',
1502
1503
  background: '生成标题、摘要等后台小活,建议选便宜的',
1503
1504
  ask: '「问一问」和解释,建议选便宜快速的;留空 = sonnet',
1504
1505
  voice: '语音转成书面文字,建议选便宜的;留空 = 同问一问',
1506
+ vision: '主任务模型看不了图时,先用它把聊天框发的图片转成文字再交给主模型(多花几秒,可在聊天里点开核对);留空 = 不转',
1505
1507
  };
1506
1508
  async function loadSceneModels() {
1507
1509
  let d; try { d = await (await fetch('/api/models')).json(); } catch { return; }
@@ -1510,26 +1512,35 @@
1510
1512
  const ms = (d.models || []).filter(m => !m.id.includes('*'));
1511
1513
  const t = d.token, quota = t && (t.dailyLimit != null || t.monthlyLimit != null)
1512
1514
  ? ` 积分:${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 + '),可以手填模型名。'
1515
+ // 没配自定义网关:不提供模型选择(全部跟随 Claude Code 默认),整组只留一句说明
1516
+ const noGw = !d.gateway;
1517
+ document.getElementById('set-models-rows').style.display = noGw ? 'none' : '';
1518
+ document.getElementById('set-models-save').parentElement.style.display = noGw ? 'none' : '';
1519
+ if (noGw) { src.textContent = '没有配置自定义网关:所有任务使用 Claude Code 默认模型,不需要选模型。配置网关后,可在这里按场景(主任务 / 计划 / 问一问 / 看图…)从网关模型里选。'; return; }
1520
+ src.textContent = d.error ? '网关模型列表读取失败(' + d.error + '),可以手填模型名。'
1515
1521
  : `网关上有 ${ms.length} 个模型${d.scoped ? '(这个 token 可用的)' : '(网关是旧版,未按 token 过滤,个别模型可能用不了)'},点输入框可选,也可以手填。` + quota;
1516
1522
  // 两份候选:主会话 / 子代理只列能用工具干活的;问一问、语音整理、后台小任务全列,便宜的排前面
1517
1523
  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('');
1524
+ const aliases = ''; // 模型只从网关目录选(不再列 Claude 别名)
1519
1525
  const cheapFirst = [...ms].sort((a, b) => ((b.tags || []).includes('cheap') - (a.tags || []).includes('cheap')));
1520
1526
  document.getElementById('set-models-dl-tools').innerHTML = ms.filter(m => !m.fit?.main).map(opt).join('') + aliases;
1521
1527
  document.getElementById('set-models-dl-all').innerHTML = cheapFirst.map(opt).join('') + aliases;
1522
- const TOOL_SCENES = { main: 1, subagent: 1 };
1528
+ document.getElementById('set-models-dl-vision').innerHTML = cheapFirst.filter(m => (m.caps || []).includes('vision')).map(opt).join('') + aliases;
1529
+ const TOOL_SCENES = { main: 1, plan: 1, subagent: 1, vision: 1 };
1530
+ const DL = (k) => k === 'vision' ? 'set-models-dl-vision' : TOOL_SCENES[k] ? 'set-models-dl-tools' : 'set-models-dl-all';
1523
1531
  document.getElementById('set-models-rows').innerHTML = Object.entries(d.sceneLabels || {}).map(([k, label]) =>
1524
1532
  `<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>
1533
+ <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
1534
  <div class="set-warn" data-warn="${esc(k)}" style="display:none;font-size:0.72rem;color:var(--yellow,#d29922)"></div></div>`).join('');
1527
1535
  // 填了不适合这个场景的模型(不支持工具等)→ 输入框下提示原因(不硬拦:网关元数据可能没填全)
1528
1536
  const byId = Object.fromEntries(ms.map(m => [m.id, m]));
1529
1537
  const check = (inp) => {
1530
1538
  const k = inp.dataset.scene, w = document.querySelector(`[data-warn="${k}"]`);
1531
1539
  const why = TOOL_SCENES[k] && byId[inp.value.trim()]?.fit?.[k];
1532
- w.textContent = why ? '⚠ ' + why : ''; w.style.display = why ? '' : 'none';
1540
+ // 看图:主任务模型本身看不了图却没配看图模型 → 提醒(图片发过去 AI 会读不出来)
1541
+ const mainM = byId[(document.querySelector('[data-scene="main"]')?.value || '').trim()];
1542
+ const hint = k === 'vision' && !inp.value.trim() && mainM?.caps?.length && !mainM.caps.includes('vision') ? `主任务模型 ${mainM.name} 看不了图,建议配一个,否则发图片 AI 会读不出来` : '';
1543
+ w.textContent = why ? '⚠ ' + why : hint ? '⚠ ' + hint : ''; w.style.display = why || hint ? '' : 'none';
1533
1544
  };
1534
1545
  document.querySelectorAll('#set-models-rows [data-scene]').forEach(i => { check(i); i.addEventListener('input', () => check(i)); });
1535
1546
  // 和其他设置项一样:改了才出现「保存」
Binary file