@wendongfly/myhi 1.3.204 → 1.3.206

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.gz CHANGED
Binary file
package/dist/index.html CHANGED
@@ -181,6 +181,7 @@
181
181
  @media (max-width: 540px) {
182
182
  .set-item.inp { flex-direction: column; align-items: stretch; gap: 0.4rem; }
183
183
  .set-item.inp .set-ctl { flex: none; }
184
+ .set-item.inp .set-seg { flex: none; } /* 访问协议 / 升级下载源:说明长,名称在上、分段条整行(以前被挤成一字一行) */
184
185
  .set-lbl small { display: inline; margin-left: 0.4rem; }
185
186
  .set-seg { flex: 1; }
186
187
  }
@@ -955,6 +956,19 @@
955
956
  <button class="set-link" onclick="closeSettings();showUsage()">📊 用量 <small>费用 / token 统计</small></button>
956
957
  </div>
957
958
 
959
+ <!-- 场景模型:同一个网关 + token,不同场景用不同模型(仅管理员)-->
960
+ <div id="set-models" 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-models-src">读取网关模型列表…</div>
964
+ <div id="set-models-rows"></div>
965
+ <div class="set-item" style="justify-content:flex-end"><button class="set-btn" id="set-models-save" onclick="saveSceneModels()">保存</button></div>
966
+ </div>
967
+ <div id="set-models-status" class="set-status"></div>
968
+ <datalist id="set-models-dl-tools"></datalist>
969
+ <datalist id="set-models-dl-all"></datalist>
970
+ </div>
971
+
958
972
  <div id="set-svc">
959
973
  <div class="set-group-t" id="set-svc-t">服务</div>
960
974
  <div class="set-card">
@@ -963,12 +977,12 @@
963
977
  <button id="set-up-btn" class="set-btn dirty" style="display:none" onclick="homeUpgrade()">升级</button>
964
978
  </div>
965
979
  <div id="set-up-status" class="set-status"></div>
966
- <div class="set-item admin-only" id="set-proto-row">
980
+ <div class="set-item inp admin-only" id="set-proto-row">
967
981
  <div class="set-lbl">访问协议<small id="set-proto-sub">HTTPS 才能在手机上用麦克风 / 摄像头;内网只想少点「不安全」提示可用 HTTP</small></div>
968
982
  <div class="set-seg" id="set-proto"><button data-p="https">HTTPS</button><button data-p="http">HTTP</button></div>
969
983
  </div>
970
984
  <div id="set-proto-status" class="set-status"></div>
971
- <div class="set-item admin-only">
985
+ <div class="set-item inp admin-only">
972
986
  <div class="set-lbl">升级下载源<small>myhi 和 Claude 升级从哪下载。自动:先官方源,连不上或装失败自动换国内镜像</small></div>
973
987
  <div class="set-seg" id="set-reg"><button data-r="auto">自动</button><button data-r="official">官方</button><button data-r="mirror">国内镜像</button></div>
974
988
  </div>
@@ -1122,7 +1136,7 @@
1122
1136
  document.getElementById('set-mode').innerHTML = !data.multiUser ? '👤 单用户模式 · 所有功能都可用'
1123
1137
  : _isAdmin ? '👥 多用户模式 · 你是管理员 · <a href="/admin#mode">管理成员 →</a>'
1124
1138
  : `👥 多用户模式 · 你是成员「${esc(data.name || '')}」· 本机显示名、语音等全局设置需管理员修改`;
1125
- if (!_isAdmin) document.querySelectorAll('#set-svc .admin-only').forEach(el => el.remove());
1139
+ if (!_isAdmin) document.querySelectorAll('#set-sheet .admin-only').forEach(el => el.remove());
1126
1140
  checkAiEngine();
1127
1141
  checkHomeVersion();
1128
1142
  if (data.hasUsers && data.member) { document.getElementById('passwd-btn').style.display = ''; document.getElementById('ws-menu-pw').style.display = ''; }
@@ -1315,7 +1329,7 @@
1315
1329
  window.openSettings = function() {
1316
1330
  renderAppearance();
1317
1331
  ['set-machine-status', 'set-asr-status', 'set-restart-status', 'set-up-status', 'set-proto-status'].filter(id => document.getElementById(id)).forEach(id => setStatus(id, ''));
1318
- if (_isAdmin) { loadProto(); loadRegistry(); }
1332
+ if (_isAdmin) { loadProto(); loadRegistry(); loadSceneModels(); }
1319
1333
  document.querySelectorAll('#set-sheet .set-btn').forEach(b => b.classList.remove('dirty'));
1320
1334
  document.getElementById('set-backdrop').classList.add('open');
1321
1335
  document.getElementById('set-sheet').classList.add('open');
@@ -1480,6 +1494,57 @@
1480
1494
  });
1481
1495
  if (d.fromEnv) setStatus('set-proto-status', '启动时设了环境变量 MYHI_NO_HTTPS,固定为 HTTP');
1482
1496
  }
1497
+ // ── 场景模型 ────────────────────────────────────────────
1498
+ // 每个场景一个输入框:可从网关模型列表里选(datalist),也可手填任意模型名 / 别名;留空 = 用默认
1499
+ const SCENE_HINT = {
1500
+ main: '会话里没手动选模型时用它;留空 = Claude Code 默认',
1501
+ subagent: 'AI 把任务拆成子任务时用;留空 = 跟主模型一样',
1502
+ background: '生成标题、摘要等后台小活,建议选便宜的',
1503
+ ask: '「问一问」和解释,建议选便宜快速的;留空 = sonnet',
1504
+ voice: '语音转成书面文字,建议选便宜的;留空 = 同问一问',
1505
+ };
1506
+ async function loadSceneModels() {
1507
+ let d; try { d = await (await fetch('/api/models')).json(); } catch { return; }
1508
+ if (!d.ok) return;
1509
+ const src = document.getElementById('set-models-src');
1510
+ const ms = (d.models || []).filter(m => !m.id.includes('*'));
1511
+ const t = d.token, quota = t && (t.dailyLimit != null || t.monthlyLimit != null)
1512
+ ? ` 积分:${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
+ : `网关上有 ${ms.length} 个模型${d.scoped ? '(这个 token 可用的)' : '(网关是旧版,未按 token 过滤,个别模型可能用不了)'},点输入框可选,也可以手填。` + quota;
1516
+ // 两份候选:主会话 / 子代理只列能用工具干活的;问一问、语音整理、后台小任务全列,便宜的排前面
1517
+ 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('');
1519
+ const cheapFirst = [...ms].sort((a, b) => ((b.tags || []).includes('cheap') - (a.tags || []).includes('cheap')));
1520
+ document.getElementById('set-models-dl-tools').innerHTML = ms.filter(m => !m.fit?.main).map(opt).join('') + aliases;
1521
+ document.getElementById('set-models-dl-all').innerHTML = cheapFirst.map(opt).join('') + aliases;
1522
+ const TOOL_SCENES = { main: 1, subagent: 1 };
1523
+ document.getElementById('set-models-rows').innerHTML = Object.entries(d.sceneLabels || {}).map(([k, label]) =>
1524
+ `<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>
1526
+ <div class="set-warn" data-warn="${esc(k)}" style="display:none;font-size:0.72rem;color:var(--yellow,#d29922)"></div></div>`).join('');
1527
+ // 填了不适合这个场景的模型(不支持工具等)→ 输入框下提示原因(不硬拦:网关元数据可能没填全)
1528
+ const byId = Object.fromEntries(ms.map(m => [m.id, m]));
1529
+ const check = (inp) => {
1530
+ const k = inp.dataset.scene, w = document.querySelector(`[data-warn="${k}"]`);
1531
+ const why = TOOL_SCENES[k] && byId[inp.value.trim()]?.fit?.[k];
1532
+ w.textContent = why ? '⚠ ' + why : ''; w.style.display = why ? '' : 'none';
1533
+ };
1534
+ document.querySelectorAll('#set-models-rows [data-scene]').forEach(i => { check(i); i.addEventListener('input', () => check(i)); });
1535
+ // 和其他设置项一样:改了才出现「保存」
1536
+ const btn = document.getElementById('set-models-save');
1537
+ btn.classList.remove('dirty');
1538
+ document.querySelectorAll('#set-models-rows [data-scene]').forEach(i => i.addEventListener('input', () => btn.classList.add('dirty')));
1539
+ }
1540
+ window.saveSceneModels = async function() {
1541
+ const body = {};
1542
+ document.querySelectorAll('#set-models-rows [data-scene]').forEach(i => { body[i.dataset.scene] = i.value.trim(); });
1543
+ let r; try { r = await (await fetch('/api/model-scenes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })).json(); } catch (e) { r = { ok: false, error: e.message }; }
1544
+ if (r.ok) document.getElementById('set-models-save').classList.remove('dirty');
1545
+ setStatus('set-models-status', r.ok ? '已保存。新开的任务和下一次重启的 AI 进程生效;正在跑的任务在下次重启 AI 后生效' : '保存失败:' + (r.error || ''));
1546
+ };
1547
+
1483
1548
  async function loadRegistry() {
1484
1549
  let d; try { d = await (await fetch('/api/npm-registry')).json(); } catch { return; }
1485
1550
  const seg = document.getElementById('set-reg'); if (!seg || !d.ok) return;
Binary file