@wendongfly/myhi 1.3.77 → 1.3.81

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
@@ -695,6 +695,9 @@
695
695
  const inputBox = document.getElementById('input-box');
696
696
  const shortcutBar = document.getElementById('shortcut-bar');
697
697
 
698
+ // 恢复本会话的闹钟(跨刷新持久化)
699
+ restoreReminders();
700
+
698
701
  // textarea 自适应高度
699
702
  cmdInput.addEventListener('input', () => {
700
703
  cmdInput.style.height = 'auto';
@@ -965,7 +968,142 @@
965
968
  }
966
969
 
967
970
  // 当收到非 assistant 类型的消息时,结束流式追加
968
- function endStream() { _streamEl = null; _streamText = ''; }
971
+ function endStream() {
972
+ if (_streamEl && chatArea.contains(_streamEl)) {
973
+ maybeAttachReminderChip(_streamEl, _streamText);
974
+ }
975
+ _streamEl = null;
976
+ _streamText = '';
977
+ }
978
+
979
+ // ── 闹钟 chip ──────────────────────────────────
980
+ // 扫描 assistant 气泡里"过 N 分钟/小时后..."的意图,加"🔔 提醒我"按钮
981
+ // 点了就 setTimeout + 到点用 Notifications API 弹通知;纯客户端,localStorage 持久化跨刷新
982
+ function maybeAttachReminderChip(bubbleEl, text) {
983
+ if (!bubbleEl || bubbleEl.querySelector('.reminder-bar')) return;
984
+ // 前缀锚(过/等/再过/大约/约)或后缀锚(后/以后/later)任一命中;
985
+ // 避免"花了 15 分钟""用时 20 分"这种回顾误触。
986
+ // (?<![一-鿿]) 排除"经过 / 度过 / 已过 / 错过"这类汉字前置的回顾语义
987
+ const RE_PREFIX = /(?<![一-鿿])(?:过|等|再过|大约|约)\s*[~约]?\s*(\d+)\s*(分钟|分|min(?:utes?)?|小时|hour(?:s)?)/gi;
988
+ const RE_SUFFIX = /(\d+)\s*(分钟|分|min(?:utes?)?|小时|hour(?:s)?)\s*(?:后|以后|later)/gi;
989
+ const seen = new Set();
990
+ const chips = [];
991
+ const scan = (re) => {
992
+ let m;
993
+ while ((m = re.exec(text)) !== null) {
994
+ const num = parseInt(m[1], 10);
995
+ if (!num || num > 720) continue; // 上限 12h,防误匹配
996
+ const isHour = /小时|hour/.test(m[2]);
997
+ const seconds = isHour ? num * 3600 : num * 60;
998
+ if (seen.has(seconds)) continue;
999
+ seen.add(seconds);
1000
+ chips.push({
1001
+ seconds,
1002
+ label: isHour ? `${num} 小时后` : `${num} 分钟后`,
1003
+ snippet: text.slice(Math.max(0, m.index - 40), m.index + 80).replace(/\s+/g, ' ').trim(),
1004
+ });
1005
+ }
1006
+ };
1007
+ scan(RE_PREFIX);
1008
+ scan(RE_SUFFIX);
1009
+ if (!chips.length) return;
1010
+
1011
+ const bar = document.createElement('div');
1012
+ bar.className = 'reminder-bar';
1013
+ bar.style.cssText = 'display:flex;flex-wrap:wrap;gap:0.4rem;margin-top:0.5rem;';
1014
+ chips.forEach(c => {
1015
+ const btn = document.createElement('button');
1016
+ btn.className = 'reminder-chip';
1017
+ btn.dataset.seconds = String(c.seconds);
1018
+ btn.textContent = `🔔 ${c.label}叫我`;
1019
+ btn.style.cssText = 'background:#21262d;border:1px solid #30363d;color:#c9d1d9;border-radius:12px;padding:0.3rem 0.7rem;font-size:0.78rem;cursor:pointer;transition:background 0.15s;';
1020
+ btn.addEventListener('click', () => scheduleReminder(btn, c));
1021
+ bar.appendChild(btn);
1022
+ });
1023
+ bubbleEl.appendChild(bar);
1024
+ }
1025
+
1026
+ async function scheduleReminder(btn, spec) {
1027
+ if ('Notification' in window && Notification.permission === 'default') {
1028
+ try { await Notification.requestPermission(); } catch {}
1029
+ }
1030
+ const rec = {
1031
+ id: 'r-' + Date.now() + '-' + Math.random().toString(36).slice(2, 6),
1032
+ fireAt: Date.now() + spec.seconds * 1000,
1033
+ label: spec.label,
1034
+ snippet: spec.snippet,
1035
+ };
1036
+ saveReminder(rec);
1037
+ armReminder(rec, btn);
1038
+ }
1039
+
1040
+ function reminderKey() { return 'myhi-reminders:' + SESSION_ID; }
1041
+ function saveReminder(rec) {
1042
+ const all = JSON.parse(localStorage.getItem(reminderKey()) || '[]');
1043
+ all.push(rec);
1044
+ localStorage.setItem(reminderKey(), JSON.stringify(all));
1045
+ }
1046
+ function removeReminderFromStore(id) {
1047
+ const all = JSON.parse(localStorage.getItem(reminderKey()) || '[]');
1048
+ localStorage.setItem(reminderKey(), JSON.stringify(all.filter(r => r.id !== id)));
1049
+ }
1050
+
1051
+ function armReminder(rec, btn) {
1052
+ const remain = rec.fireAt - Date.now();
1053
+ if (btn) {
1054
+ btn.disabled = true;
1055
+ btn.style.cursor = 'default';
1056
+ btn.style.background = '#1c3a2b';
1057
+ btn.style.borderColor = '#3fb950';
1058
+ const tick = () => {
1059
+ const left = Math.max(0, rec.fireAt - Date.now());
1060
+ if (left <= 0) return;
1061
+ const min = Math.floor(left / 60000);
1062
+ const sec = Math.floor((left % 60000) / 1000);
1063
+ btn.textContent = `⏰ 剩 ${min}:${String(sec).padStart(2, '0')}`;
1064
+ };
1065
+ tick();
1066
+ const iv = setInterval(tick, 1000);
1067
+ setTimeout(() => clearInterval(iv), Math.max(0, remain) + 100);
1068
+ }
1069
+ // setTimeout 最大 32-bit 有符号整数 = ~24.8 天,够用;负数立即触发
1070
+ setTimeout(() => fireReminder(rec, btn), Math.max(0, remain));
1071
+ }
1072
+
1073
+ function fireReminder(rec, btn) {
1074
+ removeReminderFromStore(rec.id);
1075
+ if (btn) {
1076
+ btn.textContent = '✅ 到点了';
1077
+ btn.style.background = '#1c3a2b';
1078
+ }
1079
+ const body = rec.snippet ? `你之前提到:${rec.snippet.slice(0, 100)}${rec.snippet.length > 100 ? '…' : ''}` : `${rec.label}闹钟到点`;
1080
+ try {
1081
+ if ('Notification' in window && Notification.permission === 'granted') {
1082
+ const n = new Notification('myhi 提醒', { body, tag: rec.id });
1083
+ n.onclick = () => { window.focus(); n.close(); };
1084
+ }
1085
+ } catch {}
1086
+ try { navigator.vibrate && navigator.vibrate([300, 100, 300]); } catch {}
1087
+ // 输入框为空时自动填一个提示文案,用户校对后发送
1088
+ if (cmdInput && !cmdInput.value) {
1089
+ const excerpt = (rec.snippet || '').slice(0, 60).replace(/["'`]/g, '');
1090
+ cmdInput.value = `${rec.label}到了${excerpt ? ',你之前说:' + excerpt + '…' : ''},麻烦查一下`;
1091
+ cmdInput.focus();
1092
+ cmdInput.style.height = 'auto';
1093
+ cmdInput.style.height = Math.min(140, cmdInput.scrollHeight) + 'px';
1094
+ }
1095
+ }
1096
+
1097
+ function restoreReminders() {
1098
+ try {
1099
+ const all = JSON.parse(localStorage.getItem(reminderKey()) || '[]');
1100
+ // 已过期超 1 小时的丢弃(避免打开老 session 被一堆过期通知轰炸)
1101
+ const cutoff = Date.now() - 60 * 60 * 1000;
1102
+ const kept = all.filter(r => r.fireAt > cutoff);
1103
+ if (kept.length !== all.length) localStorage.setItem(reminderKey(), JSON.stringify(kept));
1104
+ kept.forEach(rec => armReminder(rec, null));
1105
+ } catch {}
1106
+ }
969
1107
 
970
1108
  let _toolGroupEl = null; // 当前工具组容器
971
1109
  let _toolGroupCount = 0;
@@ -1221,11 +1359,9 @@
1221
1359
  }
1222
1360
 
1223
1361
  // ── 工作状态 ──────────────────────────────────
1224
- let workStateTimer = null;
1225
1362
  function setWorkState(state) {
1226
1363
  if (workState === state) return;
1227
1364
  workState = state;
1228
- clearTimeout(workStateTimer);
1229
1365
  const el = document.getElementById('work-status');
1230
1366
  const STATES = {
1231
1367
  idle: { text: '空闲', dot: 'idle' },
@@ -1235,9 +1371,8 @@
1235
1371
  };
1236
1372
  const s = STATES[state] || STATES.idle;
1237
1373
  el.innerHTML = `<span class="sb-dot ${s.dot}"></span> ${s.text}`;
1238
- // 自动回归空闲
1239
- if (state === 'working') workStateTimer = setTimeout(() => setWorkState('idle'), 5000);
1240
- // Agent 模式下忙时显示取消按钮,隐藏发送按钮
1374
+ // 回归 idle 由 result / agent:busy false / session-exit 驱动,
1375
+ // 不做定时兜底——长工具调用期间兜底会让取消按钮提前消失。
1241
1376
  if (currentSession?.mode === 'agent') {
1242
1377
  const busy = state !== 'idle';
1243
1378
  cancelBtn.classList.toggle('show', busy);
@@ -1428,6 +1563,8 @@
1428
1563
  if (msg.result && !_textRenderedThisTurn) {
1429
1564
  addAssistantMessage(typeof msg.result === 'string' ? msg.result : JSON.stringify(msg.result));
1430
1565
  }
1566
+ // 轮次结束时确保闹钟 chip 被扫描(防止最后一条消息未经过 endStream)
1567
+ endStream();
1431
1568
  const cost = msg.total_cost_usd ? ` ($${msg.total_cost_usd.toFixed(4)})` : '';
1432
1569
  addStatusMessage(`完成${cost}`);
1433
1570
  break;
@@ -3000,8 +3137,8 @@
3000
3137
  const img = new Image(); const url = URL.createObjectURL(srcBlob);
3001
3138
  const blob = await new Promise((resolve, reject) => {
3002
3139
  img.onload = () => { URL.revokeObjectURL(url); let {width:w, height:h} = img;
3003
- // 压缩策略:长边限制 1024px,JPEG 质量 0.7,适合截图分析
3004
- const M = 1024;
3140
+ // 压缩策略:长边限制 768px,JPEG 质量 0.7,适合截图分析且降低图片累积压力
3141
+ const M = 768;
3005
3142
  if(w>M||h>M){if(w>=h){h=Math.round(h*M/w);w=M}else{w=Math.round(w*M/h);h=M}}
3006
3143
  const c=document.createElement('canvas');c.width=w;c.height=h;c.getContext('2d').drawImage(img,0,0,w,h);
3007
3144
  c.toBlob(b=>b?resolve(b):reject(new Error('toBlob failed')),'image/jpeg',0.7); };