@wendongfly/myhi 1.3.45 → 1.3.47

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.
Files changed (2) hide show
  1. package/dist/chat.html +123 -32
  2. package/package.json +1 -1
package/dist/chat.html CHANGED
@@ -237,6 +237,13 @@
237
237
  .action-sheet-item:active { background: #30363d; }
238
238
  .action-sheet-item .desc { font-size: 0.72rem; color: #8b949e; }
239
239
  .action-sheet-item .check { color: #3fb950; font-size: 0.9rem; }
240
+ .ask-q-block { padding: 0.5rem 0.3rem 0.3rem; border-top: 1px solid #21262d; }
241
+ .ask-q-block:first-child { border-top: none; }
242
+ .ask-q-label { font-size: 0.72rem; color: #8b949e; font-weight: 600; margin-bottom: 0.25rem; letter-spacing: 0.05em; }
243
+ .ask-q-text { font-size: 0.82rem; color: #c9d1d9; line-height: 1.5; white-space: pre-wrap; margin-bottom: 0.4rem; }
244
+ .ask-opt-selected { background: #1f3a5f !important; border: 1px solid #388bfd; }
245
+ .ask-opt-selected .check { color: #58a6ff; }
246
+ .ask-submit-btn:disabled { background: #30363d !important; color: #6e7681 !important; cursor: not-allowed; }
240
247
  .action-sheet-cancel { display: block; width: 100%; margin-top: 0.5rem; padding: 0.65rem; background: #21262d; border: none; border-radius: 10px; color: #8b949e; font-size: 0.85rem; cursor: pointer; text-align: center; }
241
248
  .mem-file { margin-bottom: 0.5rem; border: 1px solid #21262d; border-radius: 8px; overflow: hidden; }
242
249
  .mem-file-header { display: flex; align-items: center; gap: 0.35rem; padding: 0.4rem 0.6rem; background: #21262d; cursor: pointer; font-size: 0.75rem; color: #58a6ff; user-select: none; }
@@ -493,10 +500,11 @@
493
500
  <div class="action-sheet-box" style="max-height:70vh;display:flex;flex-direction:column">
494
501
  <div class="action-sheet-title" id="ask-sheet-title">提问</div>
495
502
  <div id="ask-sheet-body" style="flex:1;overflow-y:auto;padding:0.3rem 0;scrollbar-width:thin"></div>
496
- <div style="padding:0.3rem 0.2rem">
503
+ <div id="ask-sheet-input-wrap" style="padding:0.3rem 0.2rem">
497
504
  <input id="ask-sheet-input" class="slash-inp" placeholder="输入自定义回答(可选)" autocomplete="off">
498
505
  </div>
499
- <button class="action-sheet-cancel" style="background:#7c3aed;color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="submitAskReply()">发送自定义回答</button>
506
+ <button id="ask-sheet-submit-multi" class="action-sheet-cancel ask-submit-btn" style="display:none;background:#388bfd;color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="submitAskMulti()">提交回答</button>
507
+ <button id="ask-sheet-submit-single" class="action-sheet-cancel" style="background:#7c3aed;color:#fff;font-weight:600;margin-bottom:0.4rem" onclick="submitAskReply()">发送自定义回答</button>
500
508
  <button class="action-sheet-cancel" onclick="closeAskSheet()">取消</button>
501
509
  </div>
502
510
  </div>
@@ -800,7 +808,8 @@
800
808
  // ── 消息渲染 ──────────────────────────────────
801
809
  function addInputMessage(text) {
802
810
  endStream(); endToolGroup();
803
- _streamedTextThisMessage = false; // 新一轮用户提问开始,重置流式渲染标志
811
+ _streamedTextThisCall = false;
812
+ _textRenderedThisTurn = false;
804
813
  const msg = document.createElement('div');
805
814
  msg.className = 'msg msg-input';
806
815
  msg.innerHTML = `<div><div class="bubble">${escHtml(text)}</div><div class="meta">${formatTime(new Date())}</div></div>`;
@@ -821,7 +830,14 @@
821
830
 
822
831
  let _streamEl = null; // 当前流式输出的 assistant 元素
823
832
  let _streamText = ''; // 累积的原始文本
824
- let _streamedTextThisMessage = false; // 当前轮(自上次用户提问起)是否已渲染过答复文本(来自 content_block_delta 流式或 assistant 事件的完整 text 块);用于在末尾 assistant / result 事件中跳过重复渲染
833
+ // 拆成两个标志解耦:
834
+ // _streamedTextThisCall per-LLM-call:当前 LLM call 内是否已通过 content_block_delta 渲染过文本,
835
+ // 用于抑制同一 call 末尾 assistant 事件 text 块的重复回声。call 边界由 message_start / tool_result 重置。
836
+ // _textRenderedThisTurn per-turn:当前轮(自上次用户提问起)是否已渲染过任何答复文本,
837
+ // 用于抑制 result 事件的 msg.result 重复渲染。
838
+ // 之前合并成一个 per-turn 标志会让"非流式 + 多步工具调用"场景里第二条 assistant 事件的 text 被吃掉。
839
+ let _streamedTextThisCall = false;
840
+ let _textRenderedThisTurn = false;
825
841
 
826
842
  function addAssistantMessage(raw) {
827
843
  removeThinking(); endToolGroup();
@@ -1316,15 +1332,16 @@
1316
1332
  case 'assistant':
1317
1333
  removeThinking();
1318
1334
  if (msg.message?.content) {
1335
+ // 进入 loop 前快照:同一 assistant 事件里所有 text 块统一按入口时刻的 call 标志决定,
1336
+ // 避免渲染完第一段 text 后置位标志,导致同事件内 [text, tool_use, text] 的第二段被跳过
1337
+ const skipText = _streamedTextThisCall;
1319
1338
  for (const block of msg.message.content) {
1320
1339
  if (block.type === 'thinking' && block.thinking) {
1321
1340
  showThinking(block.thinking);
1322
1341
  } else if (block.type === 'text' && block.text) {
1323
- if (!_streamedTextThisMessage) {
1342
+ if (!skipText) {
1324
1343
  addAssistantMessage(block.text);
1325
- // 非流式适配器(如 MiniMax)不会发 content_block_delta,
1326
- // 这里渲染后也要置位,避免后续 result 事件再渲染同样的文本
1327
- _streamedTextThisMessage = true;
1344
+ _textRenderedThisTurn = true;
1328
1345
  }
1329
1346
  } else if (block.type === 'tool_use') {
1330
1347
  if (block.name === 'AskUserQuestion' && block.input) {
@@ -1342,9 +1359,9 @@
1342
1359
  case 'result':
1343
1360
  removeThinking();
1344
1361
  setWorkState('idle');
1345
- // msg.result 是整轮的最终答复文本,纯文字回答时与流式 delta 内容相同;
1346
- // 已通过 content_block_delta 渲染过就跳过,避免生成第二个独立气泡
1347
- if (msg.result && !_streamedTextThisMessage) {
1362
+ // msg.result 是整轮的最终答复文本,纯文字回答时与本轮已渲染的文本重复;
1363
+ // 本轮已经通过流式或 assistant 事件渲染过就跳过,避免生成第二个独立气泡
1364
+ if (msg.result && !_textRenderedThisTurn) {
1348
1365
  addAssistantMessage(typeof msg.result === 'string' ? msg.result : JSON.stringify(msg.result));
1349
1366
  }
1350
1367
  const cost = msg.total_cost_usd ? ` ($${msg.total_cost_usd.toFixed(4)})` : '';
@@ -1362,7 +1379,8 @@
1362
1379
  // 流式内容增量
1363
1380
  if (msg.delta?.type === 'text_delta' && msg.delta.text) {
1364
1381
  appendStream(msg.delta.text);
1365
- _streamedTextThisMessage = true;
1382
+ _streamedTextThisCall = true;
1383
+ _textRenderedThisTurn = true;
1366
1384
  } else if (msg.delta?.type === 'thinking_delta' && msg.delta.thinking) {
1367
1385
  appendThinking(msg.delta.thinking);
1368
1386
  } else if (msg.delta?.type === 'input_json_delta' && msg.delta.partial_json) {
@@ -1373,6 +1391,8 @@
1373
1391
  endStream();
1374
1392
  break;
1375
1393
  case 'message_start':
1394
+ // 新 LLM call 边界:重置 call 标志,让本 call 的 assistant 文本能正常渲染(同一轮内可能有多次 call)
1395
+ _streamedTextThisCall = false;
1376
1396
  setWorkState('working');
1377
1397
  break;
1378
1398
  case 'message_delta':
@@ -1389,7 +1409,9 @@
1389
1409
  setWorkState('working');
1390
1410
  break;
1391
1411
  case 'tool_result':
1392
- // 独立工具结果消息
1412
+ // 独立工具结果消息。同时作为 call 边界兜底:如果某适配器没发 message_start,
1413
+ // 这里重置 call 标志能保证后续 assistant 文本不被前一个 call 的标志误吃
1414
+ _streamedTextThisCall = false;
1393
1415
  if (msg.content) {
1394
1416
  const text = typeof msg.content === 'string' ? msg.content
1395
1417
  : Array.isArray(msg.content) ? msg.content.map(b => b.text || '').join('\n')
@@ -1459,7 +1481,9 @@
1459
1481
  el.appendChild(content);
1460
1482
  chatArea.appendChild(el);
1461
1483
  _streamEl = el;
1462
- _streamedTextThisMessage = true;
1484
+ // 还原后的气泡相当于"本 call 已渲染过文本",避免回放完成后紧接着又收到的 assistant 事件再渲一次
1485
+ _streamedTextThisCall = true;
1486
+ _textRenderedThisTurn = true;
1463
1487
  }
1464
1488
 
1465
1489
  scrollToBottom();
@@ -1953,47 +1977,114 @@
1953
1977
  let currentModelId = '';
1954
1978
 
1955
1979
  // ── AskUserQuestion 弹窗 ──
1980
+ // 多题模式状态:{ qList, answers: [{ question, label } | null] }
1981
+ let askMultiState = null;
1956
1982
  window.openAskSheet = function(input) {
1957
1983
  const questions = input?.questions || input?.question;
1958
1984
  const sheet = document.getElementById('ask-sheet');
1959
1985
  const title = document.getElementById('ask-sheet-title');
1960
1986
  const body = document.getElementById('ask-sheet-body');
1961
1987
  const inp = document.getElementById('ask-sheet-input');
1988
+ const inputWrap = document.getElementById('ask-sheet-input-wrap');
1989
+ const btnMulti = document.getElementById('ask-sheet-submit-multi');
1990
+ const btnSingle = document.getElementById('ask-sheet-submit-single');
1962
1991
  body.innerHTML = '';
1963
1992
  inp.value = '';
1964
1993
 
1965
- // 支持单个 question questions 数组
1966
- const qList = Array.isArray(questions) ? questions : [{ question: questions || input?.question || '请选择', options: input?.options }];
1994
+ // 区分单题/多题。questions 是非空数组且 length>1 才进入批量模式
1995
+ const isMulti = Array.isArray(questions) && questions.length > 1;
1996
+ const qList = Array.isArray(questions)
1997
+ ? questions
1998
+ : [{ question: questions || input?.question || '请选择', options: input?.options, header: input?.header }];
1999
+
2000
+ // 标题:多题用首题 header 或通用文案,单题保持原行为
2001
+ title.textContent = (qList[0] && qList[0].header) || input?.header || (isMulti ? `提问(${qList.length} 题)` : '提问');
1967
2002
 
1968
- qList.forEach(q => {
2003
+ if (isMulti) {
2004
+ askMultiState = { qList, answers: new Array(qList.length).fill(null) };
2005
+ inputWrap.style.display = 'none';
2006
+ btnSingle.style.display = 'none';
2007
+ btnMulti.style.display = '';
2008
+ } else {
2009
+ askMultiState = null;
2010
+ inputWrap.style.display = '';
2011
+ btnSingle.style.display = '';
2012
+ btnMulti.style.display = 'none';
2013
+ }
2014
+
2015
+ qList.forEach((q, qIdx) => {
1969
2016
  const qText = q.question || q.text || '';
1970
- title.textContent = q.header || '提问';
1971
- // 问题描述
2017
+ const block = document.createElement('div');
2018
+ block.className = isMulti ? 'ask-q-block' : '';
2019
+
2020
+ if (isMulti) {
2021
+ const label = document.createElement('div');
2022
+ label.className = 'ask-q-label';
2023
+ label.textContent = `Q${qIdx + 1}`;
2024
+ block.appendChild(label);
2025
+ }
1972
2026
  if (qText) {
1973
2027
  const desc = document.createElement('div');
1974
- desc.style.cssText = 'padding:0.5rem 0.3rem;font-size:0.82rem;color:#c9d1d9;line-height:1.5;white-space:pre-wrap';
1975
- desc.textContent = qText;
1976
- body.appendChild(desc);
2028
+ if (isMulti) {
2029
+ desc.className = 'ask-q-text';
2030
+ desc.textContent = qText;
2031
+ } else {
2032
+ desc.style.cssText = 'padding:0.5rem 0.3rem;font-size:0.82rem;color:#c9d1d9;line-height:1.5;white-space:pre-wrap';
2033
+ desc.textContent = qText;
2034
+ }
2035
+ block.appendChild(desc);
1977
2036
  }
1978
- // 选项列表
1979
2037
  if (q.options?.length) {
1980
- q.options.forEach(opt => {
2038
+ q.options.forEach((opt, optIdx) => {
1981
2039
  const item = document.createElement('div');
1982
2040
  item.className = 'action-sheet-item';
1983
- item.innerHTML = `<div><div>${escHtml(opt.label || opt.value || opt)}</div>${opt.description ? `<div class="desc">${escHtml(opt.description)}</div>` : ''}</div>`;
2041
+ const label = opt.label || opt.value || String(opt);
2042
+ item.innerHTML = `<div><div>${escHtml(label)}</div>${opt.description ? `<div class="desc">${escHtml(opt.description)}</div>` : ''}</div><span class="check" style="display:none">✓</span>`;
1984
2043
  item.onclick = () => {
1985
- closeAskSheet();
1986
- const reply = opt.label || opt.value || String(opt);
1987
- addInputMessage(reply);
1988
- socket.emit('agent:query', { prompt: reply });
1989
- showThinking();
2044
+ if (isMulti) {
2045
+ // 单选切换:清掉同题其它选中态,标记当前
2046
+ Array.from(block.querySelectorAll('.action-sheet-item')).forEach(el => {
2047
+ el.classList.remove('ask-opt-selected');
2048
+ const c = el.querySelector('.check'); if (c) c.style.display = 'none';
2049
+ });
2050
+ item.classList.add('ask-opt-selected');
2051
+ const c = item.querySelector('.check'); if (c) c.style.display = '';
2052
+ askMultiState.answers[qIdx] = { question: qText, label };
2053
+ refreshAskMultiSubmit();
2054
+ } else {
2055
+ closeAskSheet();
2056
+ addInputMessage(label);
2057
+ socket.emit('agent:query', { prompt: label });
2058
+ showThinking();
2059
+ }
1990
2060
  };
1991
- body.appendChild(item);
2061
+ block.appendChild(item);
1992
2062
  });
1993
2063
  }
2064
+ body.appendChild(block);
1994
2065
  });
2066
+
2067
+ if (isMulti) refreshAskMultiSubmit();
1995
2068
  sheet.classList.add('open');
1996
- inp.focus();
2069
+ if (!isMulti) inp.focus();
2070
+ };
2071
+ function refreshAskMultiSubmit() {
2072
+ const btn = document.getElementById('ask-sheet-submit-multi');
2073
+ if (!askMultiState) return;
2074
+ const remaining = askMultiState.answers.filter(a => !a).length;
2075
+ btn.disabled = remaining > 0;
2076
+ btn.textContent = remaining > 0 ? `还剩 ${remaining} 题未答` : '提交回答';
2077
+ }
2078
+ window.submitAskMulti = function() {
2079
+ if (!askMultiState) return;
2080
+ const { answers } = askMultiState;
2081
+ if (answers.some(a => !a)) return;
2082
+ const reply = answers.map((a, i) => `Q${i + 1}: ${a.question}\nA: ${a.label}`).join('\n\n');
2083
+ closeAskSheet();
2084
+ askMultiState = null;
2085
+ addInputMessage(reply);
2086
+ socket.emit('agent:query', { prompt: reply });
2087
+ showThinking();
1997
2088
  };
1998
2089
  window.closeAskSheet = function() {
1999
2090
  document.getElementById('ask-sheet').classList.remove('open');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wendongfly/myhi",
3
- "version": "1.3.45",
3
+ "version": "1.3.47",
4
4
  "description": "Web-based terminal sharing with chat UI — control your terminal from phone via LAN/Tailscale",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",