@wendongfly/myhi 1.3.130 → 1.3.138

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
@@ -153,6 +153,11 @@
153
153
  .sub-act .k { color: var(--muted); }
154
154
  .sub-summary { background: var(--surface); border: 1px solid var(--surface2); border-radius: 6px; padding: 0.4rem 0.5rem; margin-top: 0.35rem; font-size: 0.75rem; white-space: pre-wrap; word-break: break-word; max-height: 260px; overflow-y: auto; }
155
155
 
156
+ /* 往上翻看历史时来了新消息 → 底部浮动「↓ 新消息」按钮,点击回到底部 */
157
+ #new-msg-btn { position: fixed; left: 50%; transform: translateX(-50%); bottom: 92px; z-index: 60; background: var(--blue2, #4a9eff); color: #fff; border: none; border-radius: 16px; padding: 0.35rem 0.95rem; font-size: 0.78rem; font-weight: 600; cursor: pointer; box-shadow: 0 3px 10px rgba(0,0,0,0.35); display: none; }
158
+ #new-msg-btn.show { display: block; }
159
+ #new-msg-btn:active { opacity: 0.8; }
160
+
156
161
 
157
162
  /* Diff 视图 */
158
163
  .msg-diff { font-family: 'SF Mono', 'Consolas', monospace; font-size: 0.78rem; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 0.5rem 0.6rem; overflow-x: auto; line-height: 1.35; }
@@ -384,6 +389,12 @@
384
389
  <div id="input-area">
385
390
  <div id="readonly-overlay" style="display:none"></div>
386
391
  <div id="input-box">
392
+ <!-- 待发草稿提示:本轮干完(idle)且收件箱里还有本任务的草稿时浮出来 -->
393
+ <div id="draft-tip" style="display:none;align-items:center;gap:0.5rem;padding:0.4rem 0.75rem;border-bottom:1px solid var(--surface2);background:rgba(210,153,34,0.10);cursor:pointer" onclick="openInboxSheet()">
394
+ <span style="font-size:0.95rem;flex-shrink:0">📥</span>
395
+ <span id="draft-tip-text" style="flex:1;font-size:0.74rem;color:var(--yellow);overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
396
+ <button class="img-remove" onclick="event.stopPropagation();hideDraftTip()" title="暂不处理">&times;</button>
397
+ </div>
387
398
  <div id="img-preview">
388
399
  <img id="img-preview-thumb" src="" alt="">
389
400
  <span class="img-info">已添加图片,输入文字后发送</span>
@@ -412,6 +423,10 @@
412
423
  <button class="tb-btn" id="send-btn" onclick="sendCommand()" title="发送">
413
424
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
414
425
  </button>
426
+ <!-- AI 忙时顶替发送键:这轮还没完(可能在跑子代理),先把想法存成草稿 -->
427
+ <button class="tb-btn" id="draft-btn" style="display:none;color:var(--yellow)" onclick="saveDraftFromInput('busy')" title="AI 还在跑,先存为草稿,干完再发">
428
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
429
+ </button>
415
430
  <button class="tb-btn" id="cancel-btn" onclick="cancelAgent()" title="取消 (Esc)">
416
431
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/></svg>
417
432
  </button>
@@ -447,6 +462,8 @@
447
462
  <button class="sk sk-claude" onclick="doClear()">清除</button>
448
463
  <button class="sk sk-claude" onclick="openSecretsSheet()" style="color:var(--blue2)">🔑 密钥</button>
449
464
  <button class="sk sk-claude" onclick="openGuardSheet()" style="color:var(--blue2)">🛡 护栏</button>
465
+ <button class="sk sk-claude" onclick="openFollowUpSheet()" style="color:var(--blue2)">🔄 跟进</button>
466
+ <button class="sk sk-claude" id="sk-inbox" onclick="openInboxSheet()" style="color:var(--yellow)">📥 草稿<span id="inbox-badge" style="display:none;margin-left:0.2rem;background:var(--yellow);color:#000;border-radius:8px;padding:0 0.32rem;font-size:0.66rem;font-weight:700"></span></button>
450
467
  <button class="sk sk-claude" onclick="openGitSheet()" style="color:var(--green)">提交</button>
451
468
  <button class="sk sk-claude" onclick="showMemory()">记忆</button>
452
469
  <button class="sk sk-claude" onclick="openVaSheet()" style="color:var(--blue2)">语音</button>
@@ -549,12 +566,22 @@
549
566
  </div>
550
567
  </div>
551
568
 
569
+ <!-- 往上翻历史时来新消息的浮动提示,点击回到底部 -->
570
+ <button id="new-msg-btn" onclick="jumpToBottom()">↓ 新消息</button>
571
+
552
572
  <!-- 模型选择面板 -->
553
573
  <div id="model-sheet" class="action-sheet">
554
574
  <div class="action-sheet-backdrop" onclick="closeModelSheet()"></div>
555
575
  <div class="action-sheet-box">
556
576
  <div class="action-sheet-title">选择模型</div>
557
577
  <div id="model-list"></div>
578
+ <div style="border-top:1px solid var(--surface2);margin-top:0.4rem;padding-top:0.5rem">
579
+ <div style="font-size:0.7rem;color:var(--muted);padding:0 0.2rem 0.35rem;line-height:1.5">自定义/指定模型:输入别名或全名(如 <code>opus</code> / <code>haiku</code> / <code>claude-sonnet-4-6</code>),claude 出新模型也能直接指定。</div>
580
+ <div style="display:flex;gap:0.4rem">
581
+ <input id="model-custom" class="slash-inp" placeholder="输入模型名/别名后点切换" autocomplete="off" style="flex:1" onkeydown="if(event.key==='Enter')selectCustomModel()">
582
+ <button class="action-sheet-cancel" style="background:var(--blue2);color:#fff;font-weight:600;max-width:76px;margin:0" onclick="selectCustomModel()">切换</button>
583
+ </div>
584
+ </div>
558
585
  <button class="action-sheet-cancel" onclick="closeModelSheet()">取消</button>
559
586
  </div>
560
587
  </div>
@@ -695,6 +722,60 @@
695
722
  </div>
696
723
  </div>
697
724
 
725
+ <!-- 灵感收件箱(草稿队列):会话正忙时先记下想法,干完再发。每条强制归属一个任务 -->
726
+ <div id="inbox-sheet" class="action-sheet">
727
+ <div class="action-sheet-backdrop" onclick="closeInboxSheet()"></div>
728
+ <div class="action-sheet-box" style="max-height:85vh;display:flex;flex-direction:column">
729
+ <div class="action-sheet-title">📥 灵感收件箱 · 草稿</div>
730
+ <div style="font-size:0.68rem;color:var(--muted);padding:0 0.2rem 0.5rem;line-height:1.55">
731
+ AI 正忙时冒出的新想法,先<b>存成草稿</b>(可带图片/文件),等这轮干完再取回发送。
732
+ 每条草稿都<b>记着属于哪个任务</b>,过几天也认得出。草稿只存在本机,没有发给模型。
733
+ <br>勾「<b>自动接力</b>」的草稿会在本轮干完、会话空闲后<b>自动发出</b>(你人不在也跑,按记录先后排队);
734
+ <span style="color:var(--yellow)">默认关</span>,不勾就只能手动取回。被中断或出错收尾的轮次不接力。
735
+ </div>
736
+ <div style="display:flex;gap:0.4rem;padding:0.3rem 0.2rem;border-top:1px solid var(--surface2)">
737
+ <button class="action-sheet-cancel" style="flex:1;background:var(--yellow);color:#000;font-weight:600;margin:0"
738
+ onclick="saveDraftFromInput('manual')">把输入框存为草稿</button>
739
+ </div>
740
+ <div style="font-size:0.66rem;color:var(--muted);padding:0.5rem 0.2rem 0.2rem;border-top:1px solid var(--surface2)">
741
+ <b>本任务</b>(<span id="inbox-this-title"></span>)
742
+ </div>
743
+ <div id="inbox-this" style="overflow-y:auto;padding:0.2rem 0;font-size:0.8rem;scrollbar-width:thin;min-height:3rem;max-height:34vh"></div>
744
+ <div style="font-size:0.66rem;color:var(--muted);padding:0.4rem 0.2rem 0.2rem;border-top:1px solid var(--surface2)">
745
+ <a onclick="toggleInboxOthers()" style="color:var(--blue2);cursor:pointer">▸ 其它任务的草稿 <span id="inbox-others-count"></span></a>
746
+ </div>
747
+ <div id="inbox-others" style="display:none;overflow-y:auto;padding:0.2rem 0;font-size:0.8rem;scrollbar-width:thin;max-height:26vh"></div>
748
+ <button class="action-sheet-cancel" onclick="closeInboxSheet()">关闭</button>
749
+ </div>
750
+ </div>
751
+
752
+ <!-- 自动跟进(会话级):长任务完成后自动汇报进度,免用户反复问 -->
753
+ <div id="followup-sheet" class="action-sheet">
754
+ <div class="action-sheet-backdrop" onclick="closeFollowUpSheet()"></div>
755
+ <div class="action-sheet-box" style="max-height:72vh;display:flex;flex-direction:column">
756
+ <div class="action-sheet-title">🔄 自动跟进 · 本会话</div>
757
+ <div style="font-size:0.68rem;color:var(--muted);padding:0 0.2rem 0.5rem;line-height:1.55">
758
+ 交给 AI 的长任务(压缩、导入、批处理…)常要你反复问"好了没"。开启后,AI 说完一轮、会话空闲时,myhi 会<b>每隔一段时间自动替你问一次进度并让 AI 汇报</b>;AI 判断<b>全部完成会自动停</b>。
759
+ <br><span style="color:var(--yellow)">⚠️ 每次自动跟进 = 一轮真实模型调用(花额度),故最多 5 次上限、完成即停。</span>
760
+ </div>
761
+ <label style="display:flex;align-items:center;gap:0.5rem;padding:0.45rem 0.2rem;font-size:0.86rem;cursor:pointer;border-top:1px solid var(--surface2)">
762
+ <input type="checkbox" id="fu-enabled" onchange="saveFollowUp()" style="width:1.15rem;height:1.15rem"> 开启自动跟进
763
+ </label>
764
+ <div style="display:flex;align-items:center;gap:0.5rem;padding:0.3rem 0.2rem;font-size:0.82rem">
765
+ 跟进间隔:
766
+ <select id="fu-interval" class="slash-inp" onchange="saveFollowUp()" style="max-width:130px">
767
+ <option value="5">5 分钟</option>
768
+ <option value="10">10 分钟</option>
769
+ <option value="15">15 分钟</option>
770
+ <option value="30">30 分钟</option>
771
+ <option value="60">60 分钟</option>
772
+ </select>
773
+ </div>
774
+ <div id="fu-status" style="font-size:0.72rem;color:var(--muted);padding:0.3rem 0.2rem"></div>
775
+ <button class="action-sheet-cancel" onclick="closeFollowUpSheet()">关闭</button>
776
+ </div>
777
+ </div>
778
+
698
779
  <div id="memory-sheet" class="action-sheet">
699
780
  <div class="action-sheet-backdrop" onclick="closeMemorySheet()"></div>
700
781
  <div class="action-sheet-box" style="height:90vh;max-height:90vh;display:flex;flex-direction:column">
@@ -881,14 +962,17 @@
881
962
  } else {
882
963
  html += `<div ${C}><div ${H}>5 小时配额窗口</div><div ${T}>发送一条 Agent 消息后可查看</div></div>`;
883
964
  }
884
- if (data.activeSessions?.length) {
965
+ {
966
+ // 只显示【当前会话】的用量(按本页 SESSION_ID 过滤),而不是列出所有会话
967
+ const s = (data.activeSessions || []).find(x => x.id === SESSION_ID);
885
968
  html += `<div ${C}><div ${H}>当前会话</div>`;
886
- for (const s of data.activeSessions) {
887
- if (!s.usage) continue;
969
+ if (s && s.usage) {
888
970
  const u = s.usage;
889
971
  html += `<div ${T}><b style="color:var(--text)">${s.title}</b> (${s.owner || '管理员'})</div>`;
890
972
  html += `<div style="font-size:0.78rem;color:var(--muted);margin:0.2rem 0 0.3rem;line-height:1.5">查询 <b style="color:var(--text3)">${u.queryCount}</b>次 输入 <b style="color:var(--text3)">${((u.totalInputTokens+u.totalCacheReadTokens)/1000).toFixed(0)}K</b> 输出 <b style="color:var(--text3)">${(u.totalOutputTokens/1000).toFixed(0)}K</b> 费用 <b style="color:var(--text3)">$${u.totalCostUSD.toFixed(4)}</b></div>`;
891
973
  if (u.modelUsage) { for (const [m, mu] of Object.entries(u.modelUsage)) { html += `<div style="font-size:0.72rem;color:var(--dim);line-height:1.4"> ${m.replace(/claude-/,'').replace(/-\d{8}$/,'').replace(/\[.*\]/,'')} ${(mu.inputTokens/1000).toFixed(0)}K/${(mu.outputTokens/1000).toFixed(0)}K $${mu.costUSD.toFixed(4)}</div>`; } }
974
+ } else {
975
+ html += `<div ${T} style="color:var(--muted)">本会话暂无用量(发送一条消息后统计)</div>`;
892
976
  }
893
977
  html += '</div>';
894
978
  }
@@ -1242,6 +1326,7 @@
1242
1326
  // ── 消息渲染 ──────────────────────────────────
1243
1327
  function addInputMessage(text) {
1244
1328
  endStream(); endToolGroup();
1329
+ _userScrolledUp = false; hideNewMsgHint(); // 用户主动发消息 → 回到底部看回复
1245
1330
  _streamedTextThisCall = false;
1246
1331
  _textRenderedThisTurn = false;
1247
1332
  const msg = document.createElement('div');
@@ -1760,6 +1845,11 @@
1760
1845
  const busy = state !== 'idle';
1761
1846
  cancelBtn.classList.toggle('show', busy);
1762
1847
  sendBtn.style.display = busy ? 'none' : '';
1848
+ // busy 时发送键让位给「存草稿」键;回到 idle 再提示有没有待发草稿
1849
+ const draftBtn = document.getElementById('draft-btn');
1850
+ if (draftBtn) draftBtn.style.display = busy ? '' : 'none';
1851
+ // 用 try 包住:这两个绑定声明在文件后段,初始化早期调用会撞 TDZ
1852
+ try { if (!busy) _draftTipDismissed = false; renderDraftTip(); } catch {}
1763
1853
  }
1764
1854
  _postStatus(state);
1765
1855
  }
@@ -2202,8 +2292,9 @@
2202
2292
  if (chatArea.scrollTop < chatArea.scrollHeight - chatArea.clientHeight - 100) {
2203
2293
  _userScrolledUp = true;
2204
2294
  } else if (chatArea.scrollTop > chatArea.scrollHeight - chatArea.clientHeight - 20) {
2205
- // 回到接近底部 → 恢复 trim
2295
+ // 回到接近底部 → 恢复 trim + 隐藏新消息提示
2206
2296
  _userScrolledUp = false;
2297
+ hideNewMsgHint();
2207
2298
  }
2208
2299
  // 接近顶部 100px → 拉更老历史
2209
2300
  if (chatArea.scrollTop < 100 && _historyHasMore && !_historyLoading) {
@@ -2241,7 +2332,10 @@
2241
2332
  function setPendingImage(path, blobUrl) {
2242
2333
  _pendingImage = { path, blobUrl };
2243
2334
  const preview = document.getElementById('img-preview');
2244
- document.getElementById('img-preview-thumb').src = blobUrl || '';
2335
+ const thumb = document.getElementById('img-preview-thumb');
2336
+ // 从草稿取回时只剩服务器路径、没有本地 blob 预览图 —— 藏掉缩略图免得显示破图
2337
+ thumb.src = blobUrl || '';
2338
+ thumb.style.display = blobUrl ? '' : 'none';
2245
2339
  preview.classList.add('show');
2246
2340
  cmdInput.placeholder = '输入文字描述后发送,或直接发送分析图片...';
2247
2341
  cmdInput.focus();
@@ -2286,6 +2380,13 @@
2286
2380
  const text = cmdInput.value.trim();
2287
2381
  if (!text && !_pendingImage && !_pendingFile) return;
2288
2382
 
2383
+ // AI 还在跑这一轮(可能正开着子代理,整轮 result 未到,服务端 isBusy 会拒收):
2384
+ // 不再把输入连同刚上传的图片一起丢掉,转存草稿,本轮干完再取回。
2385
+ if (currentSession?.mode === 'agent' && (workState !== 'idle' || currentSession?.busy)) {
2386
+ saveDraftFromInput('busy');
2387
+ return;
2388
+ }
2389
+
2289
2390
  if (!isController) {
2290
2391
  if (canTakeControl()) { socket.emit('take-control', { sessionId: SESSION_ID }); }
2291
2392
  else { addStatusMessage('其他用户控制中,无法发送'); return; }
@@ -2317,6 +2418,13 @@
2317
2418
  : text;
2318
2419
  socket.emit('input', input + '\r');
2319
2420
  }
2421
+ // 这条是从草稿取回的 → 真的发出去了才销毁草稿
2422
+ if (_usingDraftId) {
2423
+ const did = _usingDraftId;
2424
+ _usingDraftId = null;
2425
+ fetch('/api/inbox/' + did, { method: 'DELETE' }).then(() => refreshInbox()).catch(() => {});
2426
+ }
2427
+
2320
2428
  cmdHistory.unshift(imgPath ? `[图片] ${userText}` : filePath ? `[文件] ${fileName} ${userText}` : text);
2321
2429
  if (cmdHistory.length > 100) cmdHistory.pop();
2322
2430
  historyIdx = -1;
@@ -2881,7 +2989,14 @@
2881
2989
  document.body.appendChild(overlay);
2882
2990
  };
2883
2991
 
2884
- function scrollToBottom() { requestAnimationFrame(() => { chatArea.scrollTop = chatArea.scrollHeight; }); }
2992
+ function scrollToBottom() {
2993
+ // 用户正往上翻看历史时不要打断(把他拽回底部);只弹「↓ 新消息」提示,回到底部再恢复自动滚
2994
+ if (_userScrolledUp) { showNewMsgHint(); return; }
2995
+ requestAnimationFrame(() => { chatArea.scrollTop = chatArea.scrollHeight; });
2996
+ }
2997
+ function showNewMsgHint() { const b = document.getElementById('new-msg-btn'); if (b) b.classList.add('show'); }
2998
+ function hideNewMsgHint() { const b = document.getElementById('new-msg-btn'); if (b) b.classList.remove('show'); }
2999
+ window.jumpToBottom = function() { _userScrolledUp = false; hideNewMsgHint(); requestAnimationFrame(() => { chatArea.scrollTop = chatArea.scrollHeight; }); };
2885
3000
  function trimMessages() {
2886
3001
  if (_userScrolledUp) return; // 用户在往上翻历史,禁用自动删
2887
3002
  const msgs = chatArea.querySelectorAll('.msg');
@@ -2897,15 +3012,14 @@
2897
3012
  window.closeQR = function() { document.getElementById('qr-modal').classList.remove('open'); };
2898
3013
 
2899
3014
  // ── 模型选择 ──────────────────────────────────
3015
+ // 用【别名】而非硬编码全名:/model sonnet 永远指向该家族最新版,claude 升级不用改这里。
3016
+ // 精确版本/冷门模型用面板底部「自定义模型」输入框指定全名(如 claude-sonnet-4-6)。
2900
3017
  const MODELS = [
2901
- // 当前主力(Anthropic 最新一代,2026-04 时点)
2902
- { id: 'claude-sonnet-4-6', label: 'Sonnet 4.6', desc: '默认推荐 · 速度+智能平衡 · 1M ctx' },
2903
- { id: 'claude-opus-4-7', label: 'Opus 4.7', desc: '最强推理 · agentic coding · 1M ctx' },
2904
- { id: 'claude-fable-5', label: 'Fable 5', desc: 'Claude 5 家族 · Max plan 标配(消耗较快)' },
2905
- { id: 'claude-haiku-4-5', label: 'Haiku 4.5', desc: '最快最便宜 · 200k ctx' },
2906
- // 备选(上一代,仍可用)
2907
- { id: 'claude-opus-4-6', label: 'Opus 4.6', desc: '上一代 Opus(备选)' },
2908
- { id: 'claude-sonnet-4-5', label: 'Sonnet 4.5', desc: '上一代 Sonnet(备选)' },
3018
+ { id: 'default', label: '默认', desc: '恢复 Claude 默认模型(跟随账户/订阅设置)' },
3019
+ { id: 'sonnet', label: 'Sonnet', desc: '速度+智能平衡 · 跟随最新版' },
3020
+ { id: 'opus', label: 'Opus', desc: '最强推理 · agentic coding · 跟随最新版' },
3021
+ { id: 'haiku', label: 'Haiku', desc: '最快最便宜 · 跟随最新版' },
3022
+ { id: 'fable', label: 'Fable', desc: 'Claude 5 家族(消耗较快)· 跟随最新版' },
2909
3023
  ];
2910
3024
  let currentModelId = '';
2911
3025
 
@@ -3121,6 +3235,12 @@
3121
3235
  currentModelId = m.id;
3122
3236
  addStatusMessage('模型已切换为 ' + m.label);
3123
3237
  }
3238
+ window.selectCustomModel = function() {
3239
+ const v = (document.getElementById('model-custom').value || '').trim();
3240
+ if (!v) { addStatusMessage('请输入模型名或别名'); return; }
3241
+ document.getElementById('model-custom').value = '';
3242
+ selectModel({ id: v, label: v });
3243
+ };
3124
3244
  document.getElementById('qr-modal').addEventListener('click', (e) => { if (e.target === e.currentTarget) window.closeQR(); });
3125
3245
 
3126
3246
  // ── 恢复会话 ──────────────────────────────────
@@ -3307,6 +3427,243 @@
3307
3427
  } catch {}
3308
3428
  };
3309
3429
 
3430
+ // ── 灵感收件箱(草稿队列)───────────────────────────────────
3431
+ // AI 正忙(可能还开着子代理,整轮 result 未到)时冒出的新想法:旧行为是直接
3432
+ // 报错「正在处理中」,而输入框和图片预览此刻已被清空 —— 内容连同刚上传的图片一起丢。
3433
+ // 现在转存草稿,带归属任务,干完再取回。
3434
+ let _inboxCache = { drafts: [], counts: {} };
3435
+ let _usingDraftId = null; // 取回到输入框、等发送成功后删掉的那条
3436
+ let _draftTipDismissed = false;
3437
+
3438
+ async function refreshInbox(silent = true) {
3439
+ try {
3440
+ const r = await fetch('/api/inbox');
3441
+ const d = await r.json();
3442
+ _inboxCache = { drafts: d.drafts || [], counts: d.counts || {} };
3443
+ } catch { if (!silent) addStatusMessage('读取草稿失败'); return; }
3444
+ updateInboxBadge();
3445
+ renderDraftTip();
3446
+ }
3447
+
3448
+ function myDrafts() { return _inboxCache.drafts.filter(d => d.sessionId === SESSION_ID); }
3449
+ function otherDrafts() { return _inboxCache.drafts.filter(d => d.sessionId !== SESSION_ID); }
3450
+
3451
+ function updateInboxBadge() {
3452
+ const badge = document.getElementById('inbox-badge');
3453
+ if (!badge) return;
3454
+ const n = myDrafts().length;
3455
+ badge.textContent = n || '';
3456
+ badge.style.display = n ? '' : 'none';
3457
+ }
3458
+
3459
+ // 只在本轮真的干完(idle)时提示,避免跑着活还催你发下一条
3460
+ function renderDraftTip() {
3461
+ const tip = document.getElementById('draft-tip');
3462
+ if (!tip) return;
3463
+ const n = myDrafts().length;
3464
+ const idle = workState === 'idle' && !currentSession?.busy;
3465
+ if (!n || !idle || _draftTipDismissed) { tip.style.display = 'none'; return; }
3466
+ document.getElementById('draft-tip-text').textContent =
3467
+ `本任务有 ${n} 条待发草稿 —— 点这里取回`;
3468
+ tip.style.display = 'flex';
3469
+ }
3470
+ window.hideDraftTip = function() { _draftTipDismissed = true; renderDraftTip(); };
3471
+
3472
+ window.openInboxSheet = async function() {
3473
+ document.getElementById('inbox-sheet').classList.add('open');
3474
+ await refreshInbox(false);
3475
+ renderInbox();
3476
+ };
3477
+ window.closeInboxSheet = function() {
3478
+ document.getElementById('inbox-sheet').classList.remove('open');
3479
+ };
3480
+ window.toggleInboxOthers = function() {
3481
+ const box = document.getElementById('inbox-others');
3482
+ box.style.display = box.style.display === 'none' ? '' : 'none';
3483
+ };
3484
+
3485
+ function draftPreview(d) {
3486
+ const t = (d.text || '').replace(/\s+/g, ' ').trim();
3487
+ const att = [d.images?.length ? `📷${d.images.length}` : '', d.files?.length ? `📎${d.files.length}` : '']
3488
+ .filter(Boolean).join(' ');
3489
+ return (att ? att + ' ' : '') + (t || '(仅附件)');
3490
+ }
3491
+ function draftAge(ts) {
3492
+ const s = Math.floor((Date.now() - ts) / 1000);
3493
+ if (s < 60) return '刚刚';
3494
+ if (s < 3600) return Math.floor(s / 60) + ' 分钟前';
3495
+ if (s < 86400) return Math.floor(s / 3600) + ' 小时前';
3496
+ return Math.floor(s / 86400) + ' 天前';
3497
+ }
3498
+
3499
+ function renderInbox() {
3500
+ const esc = (s) => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
3501
+ const titleEl = document.getElementById('inbox-this-title');
3502
+ if (titleEl) titleEl.textContent = currentSession?.title || '本会话';
3503
+
3504
+ const mine = myDrafts();
3505
+ document.getElementById('inbox-this').innerHTML = mine.length ? mine.map(d => `
3506
+ <div style="display:flex;gap:0.5rem;align-items:flex-start;padding:0.45rem 0.2rem;border-bottom:1px solid var(--surface2)">
3507
+ <div style="flex:1;min-width:0">
3508
+ <div style="color:var(--text);line-height:1.45;word-break:break-word">${esc(draftPreview(d))}</div>
3509
+ <div style="font-size:0.66rem;color:var(--muted);margin-top:0.15rem">
3510
+ ${draftAge(d.createdAt)}${d.source === 'busy' ? ' · 忙时自动存' : ''}
3511
+ <label style="margin-left:0.5rem;cursor:pointer;color:${d.autoSend ? 'var(--yellow)' : 'var(--muted)'}"
3512
+ title="勾上后:本轮干完、会话空闲时自动发出这条(不用你在页面前)。默认关。">
3513
+ <input type="checkbox" ${d.autoSend ? 'checked' : ''} onchange="toggleAutoSend('${d.id}', this.checked)"
3514
+ style="vertical-align:-1px"> 自动接力
3515
+ </label>
3516
+ </div>
3517
+ </div>
3518
+ <button class="action-sheet-cancel" style="margin:0;padding:0.25rem 0.5rem;font-size:0.72rem;background:var(--green);color:#fff" onclick="useDraft('${d.id}')">取回</button>
3519
+ <button class="action-sheet-cancel" style="margin:0;padding:0.25rem 0.5rem;font-size:0.72rem" onclick="deleteDraft('${d.id}')">删</button>
3520
+ </div>`).join('')
3521
+ : '<div style="color:var(--muted);font-size:0.75rem;padding:0.5rem">(本任务暂无草稿)</div>';
3522
+
3523
+ const others = otherDrafts();
3524
+ document.getElementById('inbox-others-count').textContent = others.length ? `(${others.length})` : '(0)';
3525
+ document.getElementById('inbox-others').innerHTML = others.length ? others.map(d => `
3526
+ <div style="display:flex;gap:0.5rem;align-items:flex-start;padding:0.45rem 0.2rem;border-bottom:1px solid var(--surface2)">
3527
+ <div style="flex:1;min-width:0">
3528
+ <div style="font-size:0.68rem;color:var(--blue2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
3529
+ ${esc(d.sessionTitle || '(未知任务)')}${d.projectName ? ' · ' + esc(d.projectName) : ''}${d.sessionAlive ? '' : ' <span style="color:var(--muted)">(已结束)</span>'}
3530
+ </div>
3531
+ <div style="color:var(--text2);line-height:1.4;word-break:break-word">${esc(draftPreview(d))}</div>
3532
+ <div style="font-size:0.66rem;color:var(--muted);margin-top:0.15rem">${draftAge(d.createdAt)}</div>
3533
+ </div>
3534
+ <button class="action-sheet-cancel" style="margin:0;padding:0.25rem 0.5rem;font-size:0.72rem" onclick="reassignDraft('${d.id}')" title="改投到本任务">改投</button>
3535
+ <button class="action-sheet-cancel" style="margin:0;padding:0.25rem 0.5rem;font-size:0.72rem" onclick="deleteDraft('${d.id}')">删</button>
3536
+ </div>`).join('')
3537
+ : '<div style="color:var(--muted);font-size:0.75rem;padding:0.5rem">(没有其它任务的草稿)</div>';
3538
+ }
3539
+
3540
+ // 把当前输入框(文字 + 已上传的图片/文件)整体存成草稿
3541
+ window.saveDraftFromInput = async function(source = 'manual') {
3542
+ const text = cmdInput.value.trim();
3543
+ if (!text && !_pendingImage && !_pendingFile) { addStatusMessage('输入框是空的,没什么可存'); return false; }
3544
+ const body = {
3545
+ text,
3546
+ sessionId: SESSION_ID,
3547
+ source,
3548
+ images: _pendingImage ? [{ path: _pendingImage.path, name: '图片' }] : [],
3549
+ files: _pendingFile ? [{ path: _pendingFile.path, name: _pendingFile.name }] : [],
3550
+ };
3551
+ try {
3552
+ const r = await fetch('/api/inbox', {
3553
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
3554
+ });
3555
+ const d = await r.json();
3556
+ if (d.error) { addStatusMessage('存草稿失败: ' + d.error); return false; }
3557
+ cmdInput.value = '';
3558
+ cmdInput.style.height = 'auto';
3559
+ clearPendingImage();
3560
+ clearPendingFile();
3561
+ _usingDraftId = null;
3562
+ _draftTipDismissed = false;
3563
+ await refreshInbox();
3564
+ if (document.getElementById('inbox-sheet').classList.contains('open')) renderInbox();
3565
+ addStatusMessage(source === 'busy'
3566
+ ? '📥 AI 还在跑,已把这条存进草稿(没有丢),干完后可取回发送'
3567
+ : '📥 已存进草稿');
3568
+ return true;
3569
+ } catch (e) {
3570
+ addStatusMessage('存草稿失败: ' + e.message);
3571
+ return false;
3572
+ }
3573
+ };
3574
+
3575
+ // 取回:填回输入框(可以改完再发),草稿先留着,发送成功才删
3576
+ window.useDraft = function(id) {
3577
+ const d = _inboxCache.drafts.find(x => x.id === id);
3578
+ if (!d) return;
3579
+ cmdInput.value = d.text || '';
3580
+ cmdInput.style.height = 'auto';
3581
+ cmdInput.style.height = Math.min(cmdInput.scrollHeight, 200) + 'px';
3582
+ clearPendingImage();
3583
+ clearPendingFile();
3584
+ if (d.images?.length) setPendingImage(d.images[0].path, '');
3585
+ if (d.files?.length) {
3586
+ _pendingFile = { path: d.files[0].path, name: d.files[0].name };
3587
+ const fp = document.getElementById('file-preview');
3588
+ const nameEl = document.getElementById('file-preview-name');
3589
+ if (nameEl) nameEl.textContent = _pendingFile.name;
3590
+ if (fp) fp.style.display = 'flex';
3591
+ }
3592
+ _usingDraftId = id;
3593
+ closeInboxSheet();
3594
+ cmdInput.focus();
3595
+ };
3596
+
3597
+ window.deleteDraft = async function(id) {
3598
+ try { await fetch('/api/inbox/' + id, { method: 'DELETE' }); } catch {}
3599
+ if (_usingDraftId === id) _usingDraftId = null;
3600
+ await refreshInbox();
3601
+ renderInbox();
3602
+ };
3603
+
3604
+ // 自动接力开关(逐条控制,默认关):勾上后本轮干完、会话空闲 3 秒仍空闲就自动发出。
3605
+ // 判定在服务端做(agent.js),所以人不在页面前也照跑;被中断/出错收尾的轮次不接力。
3606
+ window.toggleAutoSend = async function(id, on) {
3607
+ try {
3608
+ await fetch('/api/inbox/' + id, {
3609
+ method: 'PUT', headers: { 'Content-Type': 'application/json' },
3610
+ body: JSON.stringify({ autoSend: !!on }),
3611
+ });
3612
+ } catch {}
3613
+ await refreshInbox();
3614
+ renderInbox();
3615
+ addStatusMessage(on ? '📥 这条已设为「自动接力」:本轮干完会自动发出' : '📥 已取消自动接力');
3616
+ };
3617
+
3618
+ // 改投:会话结束了 / 当初记错任务了,把草稿挪到当前会话名下
3619
+ window.reassignDraft = async function(id) {
3620
+ try {
3621
+ await fetch('/api/inbox/' + id, {
3622
+ method: 'PUT', headers: { 'Content-Type': 'application/json' },
3623
+ body: JSON.stringify({ sessionId: SESSION_ID }),
3624
+ });
3625
+ } catch {}
3626
+ await refreshInbox();
3627
+ renderInbox();
3628
+ addStatusMessage('📥 已改投到本任务');
3629
+ };
3630
+
3631
+ // ── 自动跟进(会话级):长任务完成后自动汇报进度 ──────────────
3632
+ window.openFollowUpSheet = function() {
3633
+ document.getElementById('followup-sheet').classList.add('open');
3634
+ loadFollowUp();
3635
+ };
3636
+ window.closeFollowUpSheet = function() {
3637
+ document.getElementById('followup-sheet').classList.remove('open');
3638
+ };
3639
+ function _fuStatusText(d) {
3640
+ return d.enabled ? `已开启 · 每 ${d.intervalMinutes} 分钟 · 已跟进 ${d.count}/${d.max} 次` : '未开启';
3641
+ }
3642
+ async function loadFollowUp() {
3643
+ try {
3644
+ const r = await fetch(`/api/session/${SESSION_ID}/followup`);
3645
+ if (!r.ok) { document.getElementById('fu-status').textContent = '(此会话不支持自动跟进)'; return; }
3646
+ const d = await r.json();
3647
+ document.getElementById('fu-enabled').checked = !!d.enabled;
3648
+ document.getElementById('fu-interval').value = String(d.intervalMinutes || 15);
3649
+ document.getElementById('fu-status').textContent = _fuStatusText(d);
3650
+ } catch { document.getElementById('fu-status').textContent = '连接失败'; }
3651
+ }
3652
+ window.saveFollowUp = async function() {
3653
+ const enabled = document.getElementById('fu-enabled').checked;
3654
+ const intervalMinutes = parseInt(document.getElementById('fu-interval').value, 10) || 15;
3655
+ try {
3656
+ const r = await fetch(`/api/session/${SESSION_ID}/followup`, {
3657
+ method: 'PUT', headers: { 'Content-Type': 'application/json' },
3658
+ body: JSON.stringify({ enabled, intervalMinutes }),
3659
+ });
3660
+ const d = await r.json();
3661
+ if (d.error) { addStatusMessage(d.error); return; }
3662
+ document.getElementById('fu-status').textContent = _fuStatusText(d);
3663
+ addStatusMessage(enabled ? `🔄 自动跟进已开启(每 ${intervalMinutes} 分钟)` : '🔄 自动跟进已关闭');
3664
+ } catch { addStatusMessage('设置失败'); }
3665
+ };
3666
+
3310
3667
  // ── 隐私护栏(Layer C,去标识化):会话级自助维护 ──────────────
3311
3668
  window.openGuardSheet = function() {
3312
3669
  document.getElementById('guard-sheet').classList.add('open');
@@ -3638,7 +3995,14 @@
3638
3995
  runSkill(name);
3639
3996
  };
3640
3997
  // 加入会话后加载 skill
3641
- socket.on('joined', () => { if (currentSession?.mode === 'agent') loadSkills(); });
3998
+ socket.on('joined', () => {
3999
+ if (currentSession?.mode === 'agent') { loadSkills(); refreshInbox(); }
4000
+ });
4001
+ // 服务端自动接力发出了一条草稿 → 同步角标/列表
4002
+ socket.on('draft-relayed', () => {
4003
+ refreshInbox();
4004
+ if (document.getElementById('inbox-sheet')?.classList.contains('open')) renderInbox();
4005
+ });
3642
4006
 
3643
4007
  // ── 技能管理 ──────────────────────────────────
3644
4008
  let _editingSkill = null; // 编辑中的技能名,null 表示新建