botmux 2.22.0 → 2.22.1

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.
@@ -1,4 +1,4 @@
1
- "use strict";(()=>{var D=class{sessions=new Map;schedules=new Map;online=!0;listeners=new Set;upsertSessions(l){for(let h of l)this.sessions.set(h.sessionId,h);this.emit()}upsertSchedules(l){for(let h of l)this.schedules.set(h.id,h);this.emit()}applySse(l,h){if(l==="session.spawned")this.sessions.set(h.session.sessionId,h.session);else if(l==="session.update"){let w=this.sessions.get(h.sessionId);w&&this.sessions.set(h.sessionId,{...w,...h.patch})}else if(l==="session.exited"){let w=this.sessions.get(h.sessionId);w&&this.sessions.set(h.sessionId,{...w,status:"closed"})}else if(l==="schedule.created")this.schedules.set(h.schedule.id,h.schedule);else if(l==="schedule.updated"){let w=this.schedules.get(h.id);w&&this.schedules.set(h.id,{...w,...h.patch})}else if(l==="schedule.deleted")this.schedules.delete(h.id);else return;this.emit()}setOnline(l){this.online!==l&&(this.online=l,this.emit())}on(l){return this.listeners.add(l),()=>this.listeners.delete(l)}emit(){for(let l of this.listeners)l()}},T=new D;async function _(){let[r,l]=await Promise.all([fetch("/api/sessions").then($=>$.json()),fetch("/api/schedules").then($=>$.json())]);T.upsertSessions(r.sessions??[]),T.upsertSchedules(l.schedules??[]);let h=new EventSource("/events"),w=["session.spawned","session.update","session.exited","schedule.created","schedule.updated","schedule.deleted","schedule.fired","heartbeat"];for(let $ of w)h.addEventListener($,o=>{try{let L=JSON.parse(o.data);T.applySse($,L.body??L)}catch{}});h.onerror=()=>T.setOnline(!1),h.onopen=()=>T.setOnline(!0)}var Q=`
1
+ "use strict";(()=>{var P=class{sessions=new Map;schedules=new Map;online=!0;listeners=new Set;upsertSessions(l){for(let b of l)this.sessions.set(b.sessionId,b);this.emit()}upsertSchedules(l){for(let b of l)this.schedules.set(b.id,b);this.emit()}applySse(l,b){if(l==="session.spawned")this.sessions.set(b.session.sessionId,b.session);else if(l==="session.update"){let w=this.sessions.get(b.sessionId);w&&this.sessions.set(b.sessionId,{...w,...b.patch})}else if(l==="session.exited"){let w=this.sessions.get(b.sessionId);w&&this.sessions.set(b.sessionId,{...w,status:"closed"})}else if(l==="schedule.created")this.schedules.set(b.schedule.id,b.schedule);else if(l==="schedule.updated"){let w=this.schedules.get(b.id);w&&this.schedules.set(b.id,{...w,...b.patch})}else if(l==="schedule.deleted")this.schedules.delete(b.id);else return;this.emit()}setOnline(l){this.online!==l&&(this.online=l,this.emit())}on(l){return this.listeners.add(l),()=>this.listeners.delete(l)}emit(){for(let l of this.listeners)l()}},T=new P;async function _(){let[r,l]=await Promise.all([fetch("/api/sessions").then($=>$.json()),fetch("/api/schedules").then($=>$.json())]);T.upsertSessions(r.sessions??[]),T.upsertSchedules(l.schedules??[]);let b=new EventSource("/events"),w=["session.spawned","session.update","session.exited","schedule.created","schedule.updated","schedule.deleted","schedule.fired","heartbeat"];for(let $ of w)b.addEventListener($,a=>{try{let L=JSON.parse(a.data);T.applySse($,L.body??L)}catch{}});b.onerror=()=>T.setOnline(!1),b.onopen=()=>T.setOnline(!0)}var Q=`
2
2
  <form id="filters" class="filters">
3
3
  <input type="search" name="q" placeholder="search workingDir / title / ids" />
4
4
  <select name="cli" multiple size="4">
@@ -36,7 +36,7 @@
36
36
  <tbody></tbody>
37
37
  </table>
38
38
  <dialog id="drawer"></dialog>
39
- `;function F(r){if(!r)return"-";let l=Date.now()-r;return l<6e4?"now":l<36e5?Math.floor(l/6e4)+"m":l<864e5?Math.floor(l/36e5)+"h":Math.floor(l/864e5)+"d"}function k(r){return r.replace(/[&<>"']/g,l=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[l])}var X="\u{1FA9E}",N="\u{1F4CD}",Y="\u{1F5A5}\uFE0F";function U(r){r.innerHTML=Q;let l=r.querySelector("#sessions-table tbody"),h=r.querySelector("#filters"),w=r.querySelector("#drawer"),$=r.querySelector("#select-all"),o=r.querySelector("#bulk-bar"),L=r.querySelector("#bulk-count"),v=r.querySelector("#bulk-close"),A=r.querySelector("#bulk-clear"),M=r.querySelector("#sessions-table"),m=new Set,S="lastMessageAt",s="desc";function b(t){let e=t.status==="closed",n=m.has(t.sessionId)?"checked":"";return`<tr data-id="${k(t.sessionId)}">
39
+ `;function F(r){if(!r)return"-";let l=Date.now()-r;return l<6e4?"now":l<36e5?Math.floor(l/6e4)+"m":l<864e5?Math.floor(l/36e5)+"h":Math.floor(l/864e5)+"d"}function k(r){return r.replace(/[&<>"']/g,l=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[l])}var X="\u{1FA9E}",B="\u{1F4CD}",Y="\u{1F5A5}\uFE0F";function U(r){r.innerHTML=Q;let l=r.querySelector("#sessions-table tbody"),b=r.querySelector("#filters"),w=r.querySelector("#drawer"),$=r.querySelector("#select-all"),a=r.querySelector("#bulk-bar"),L=r.querySelector("#bulk-count"),v=r.querySelector("#bulk-close"),x=r.querySelector("#bulk-clear"),M=r.querySelector("#sessions-table"),g=new Set,E="lastMessageAt",i="desc";function y(t){let e=t.status==="closed",n=g.has(t.sessionId)?"checked":"";return`<tr data-id="${k(t.sessionId)}">
40
40
  <td><input type="checkbox" class="row-select" ${n} ${e?"disabled":""}></td>
41
41
  <td>${k(t.botName??"")}</td>
42
42
  <td><span class="badge cli-${k(t.cliId??"unknown")}">${k(t.cliId??"unknown")}</span></td>
@@ -47,7 +47,7 @@
47
47
  <td>${F(t.lastMessageAt)}</td>
48
48
  <td>${t.adopt?X:""}</td>
49
49
  <td><button class="open">\u22EF</button></td>
50
- </tr>`}function E(){let t=new FormData(h),e=(t.get("q")??"").toLowerCase(),n=t.getAll("cli"),u=t.get("status"),i=t.get("adopt"),C=!!t.get("active"),H=[...T.sessions.values()].filter(I=>!n.length||n.includes(I.cliId??"unknown")).filter(I=>!u||I.status===u).filter(I=>!i||i==="yes"==!!I.adopt).filter(I=>!C||I.status!=="closed").filter(I=>!e||JSON.stringify(I).toLowerCase().includes(e));return H.sort(d),H}function c(t,e){return e==="spawnedAt"||e==="lastMessageAt"?Number(t[e]??0):e==="adopt"?!!t.adopt:String(t[e]??"").toLowerCase()}function d(t,e){let n=c(t,S),u=c(e,S),i=0;return typeof n=="number"&&typeof u=="number"?i=n-u:typeof n=="boolean"&&typeof u=="boolean"?i=Number(n)-Number(u):i=String(n).localeCompare(String(u)),i===0&&(i=Number(t.lastMessageAt??0)-Number(e.lastMessageAt??0)),s==="asc"?i:-i}function p(){M.querySelectorAll("th[data-sort]").forEach(t=>{let e=t.dataset.sort===S;t.classList.toggle("sorted",e),t.setAttribute("aria-sort",e?s==="asc"?"ascending":"descending":"none");let n=t.textContent?.replace(/ [▲▼]$/,"").trim()??"";t.textContent=e?`${n} ${s==="asc"?"\u25B2":"\u25BC"}`:n})}function a(){let t=E();for(let e of[...m]){let n=T.sessions.get(e);(!n||n.status==="closed")&&m.delete(e)}l.innerHTML=t.map(b).join(""),p(),y(t)}function y(t){let e=m.size;o.hidden=e===0,L.textContent=`\u5DF2\u9009 ${e} \u4E2A\u4F1A\u8BDD`;let n=t.filter(i=>i.status!=="closed");if(n.length===0){$.checked=!1,$.indeterminate=!1,$.disabled=!0;return}$.disabled=!1;let u=n.filter(i=>m.has(i.sessionId)).length;$.checked=u===n.length,$.indeterminate=u>0&&u<n.length}function g(t){let e=t.status==="closed";w.innerHTML=`
50
+ </tr>`}function S(){let t=new FormData(b),e=(t.get("q")??"").toLowerCase(),n=t.getAll("cli"),u=t.get("status"),d=t.get("adopt"),H=!!t.get("active"),C=[...T.sessions.values()].filter(I=>!n.length||n.includes(I.cliId??"unknown")).filter(I=>!u||I.status===u).filter(I=>!d||d==="yes"==!!I.adopt).filter(I=>!H||I.status!=="closed").filter(I=>!e||JSON.stringify(I).toLowerCase().includes(e));return C.sort(c),C}function o(t,e){return e==="spawnedAt"||e==="lastMessageAt"?Number(t[e]??0):e==="adopt"?!!t.adopt:String(t[e]??"").toLowerCase()}function c(t,e){let n=o(t,E),u=o(e,E),d=0;return typeof n=="number"&&typeof u=="number"?d=n-u:typeof n=="boolean"&&typeof u=="boolean"?d=Number(n)-Number(u):d=String(n).localeCompare(String(u)),d===0&&(d=Number(t.lastMessageAt??0)-Number(e.lastMessageAt??0)),i==="asc"?d:-d}function p(){M.querySelectorAll("th[data-sort]").forEach(t=>{let e=t.dataset.sort===E;t.classList.toggle("sorted",e),t.setAttribute("aria-sort",e?i==="asc"?"ascending":"descending":"none");let n=t.textContent?.replace(/ [▲▼]$/,"").trim()??"";t.textContent=e?`${n} ${i==="asc"?"\u25B2":"\u25BC"}`:n})}function s(){let t=S();for(let e of[...g]){let n=T.sessions.get(e);(!n||n.status==="closed")&&g.delete(e)}l.innerHTML=t.map(y).join(""),p(),f(t)}function f(t){let e=g.size;a.hidden=e===0,L.textContent=`\u5DF2\u9009 ${e} \u4E2A\u4F1A\u8BDD`;let n=t.filter(d=>d.status!=="closed");if(n.length===0){$.checked=!1,$.indeterminate=!1,$.disabled=!0;return}$.disabled=!1;let u=n.filter(d=>g.has(d.sessionId)).length;$.checked=u===n.length,$.indeterminate=u>0&&u<n.length}function h(t){let e=t.status==="closed";w.innerHTML=`
51
51
  <article>
52
52
  <header>
53
53
  <h3>${k(t.title??t.sessionId)}</h3>
@@ -59,15 +59,15 @@
59
59
  ${t.threadId?`<p><b>threadId:</b> <code>${k(t.threadId)}</code></p>`:""}
60
60
  <p><b>workingDir:</b> ${k(t.workingDir??"-")}</p>
61
61
  <div class="actions">
62
- <button id="locate-btn" type="button">${N} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898</button>
62
+ <button id="locate-btn" type="button">${B} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898</button>
63
63
  ${t.webPort?`<a class="btn-link" href="http://${k(location.hostname)}:${t.webPort}" target="_blank">${Y} \u6253\u5F00 xterm</a>`:""}
64
64
  ${e?"":'<button id="close-btn" type="button" class="contrast">\u5173\u95ED\u4F1A\u8BDD</button>'}
65
65
  </div>
66
66
  <form method="dialog"><button>\u5173\u95ED</button></form>
67
- </article>`,w.querySelectorAll("[data-copy]").forEach(i=>{i.onclick=()=>{navigator.clipboard.writeText(i.dataset.copy??""),i.textContent="copied",setTimeout(()=>{i.textContent="copy"},800)}});let n=w.querySelector("#locate-btn");n&&(n.onclick=async()=>{n.disabled=!0,n.textContent=`${N} \u53D1\u9001\u4E2D...`;try{let i=await fetch(`/api/sessions/${encodeURIComponent(t.sessionId)}/locate`,{method:"POST"}),C=await i.json();if(C.ok){let H=30;n.textContent=`${N} (\u51B7\u5374 ${H}s)`;let I=setInterval(()=>{H-=1,H<=0?(clearInterval(I),n.disabled=!1,n.textContent=`${N} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898`):n.textContent=`${N} (\u51B7\u5374 ${H}s)`},1e3)}else{let H=C.error??i.status;alert("Locate failed: "+H),n.disabled=!1,n.textContent=`${N} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898`}}catch(i){alert("Locate error: "+i),n.disabled=!1,n.textContent=`${N} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898`}});let u=w.querySelector("#close-btn");u&&(u.onclick=async()=>{if(confirm("\u5173\u95ED\u8FD9\u4E2A\u4F1A\u8BDD?")){u.disabled=!0;try{await fetch(`/api/sessions/${encodeURIComponent(t.sessionId)}/close`,{method:"POST"})}finally{w.close()}}}),w.showModal()}l.addEventListener("click",t=>{let e=t.target;if(e.classList.contains("row-select")){let H=e.closest("tr[data-id]");if(!H)return;let I=H.dataset.id;e.checked?m.add(I):m.delete(I),y(E());return}let n=e.closest("td");if(n&&n.querySelector(".row-select"))return;let u=e.closest("tr[data-id]");if(!u)return;let i=u.dataset.id,C=T.sessions.get(i);C&&g(C)}),$.addEventListener("change",()=>{let t=E().filter(e=>e.status!=="closed");if($.checked)for(let e of t)m.add(e.sessionId);else for(let e of t)m.delete(e.sessionId);a()}),A.addEventListener("click",()=>{m.clear(),a()}),v.addEventListener("click",async()=>{let t=[...m];if(t.length===0||!confirm(`\u5173\u95ED\u9009\u4E2D\u7684 ${t.length} \u4E2A\u4F1A\u8BDD\uFF1F`))return;v.disabled=!0,A.disabled=!0;let e=v.textContent,n=0,u=0,i=[];v.textContent=`\u5173\u95ED\u4E2D 0/${t.length}...`;let C=[...t];async function H(){for(;C.length;){let I=C.shift();try{let x=await fetch(`/api/sessions/${encodeURIComponent(I)}/close`,{method:"POST"}),R=null;try{R=await x.json()}catch{}if(!x.ok||R?.ok===!1){u+=1;let W=R?.error??`HTTP ${x.status}`;i.push(`${I.slice(0,12)}\u2026: ${W}`)}}catch(x){u+=1,i.push(`${I.slice(0,12)}\u2026: ${x?.message??x}`)}finally{n+=1,v.textContent=`\u5173\u95ED\u4E2D ${n}/${t.length}...`}}}if(await Promise.all(Array.from({length:Math.min(6,t.length)},()=>H())),v.textContent=e,v.disabled=!1,A.disabled=!1,m.clear(),a(),u>0){let I=i.slice(0,3).join(`
68
- `),x=i.length>3?`
69
- ... +${i.length-3} \u4E2A`:"";alert(`\u5173\u95ED\u5B8C\u6210\uFF1A\u6210\u529F ${t.length-u} / \u5931\u8D25 ${u}
70
- ${I}${x}`)}}),M.querySelectorAll("th[data-sort]").forEach(t=>{t.addEventListener("click",()=>{let e=t.dataset.sort;S===e?s=s==="asc"?"desc":"asc":(S=e,s=e==="spawnedAt"||e==="lastMessageAt"?"desc":"asc"),a()})}),h.addEventListener("input",a),T.on(a),a()}var Z=`
67
+ </article>`,w.querySelectorAll("[data-copy]").forEach(d=>{d.onclick=()=>{navigator.clipboard.writeText(d.dataset.copy??""),d.textContent="copied",setTimeout(()=>{d.textContent="copy"},800)}});let n=w.querySelector("#locate-btn");n&&(n.onclick=async()=>{n.disabled=!0,n.textContent=`${B} \u53D1\u9001\u4E2D...`;try{let d=await fetch(`/api/sessions/${encodeURIComponent(t.sessionId)}/locate`,{method:"POST"}),H=await d.json();if(H.ok){let C=30;n.textContent=`${B} (\u51B7\u5374 ${C}s)`;let I=setInterval(()=>{C-=1,C<=0?(clearInterval(I),n.disabled=!1,n.textContent=`${B} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898`):n.textContent=`${B} (\u51B7\u5374 ${C}s)`},1e3)}else{let C=H.error??d.status;alert("Locate failed: "+C),n.disabled=!1,n.textContent=`${B} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898`}}catch(d){alert("Locate error: "+d),n.disabled=!1,n.textContent=`${B} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898`}});let u=w.querySelector("#close-btn");u&&(u.onclick=async()=>{if(confirm("\u5173\u95ED\u8FD9\u4E2A\u4F1A\u8BDD?")){u.disabled=!0;try{await fetch(`/api/sessions/${encodeURIComponent(t.sessionId)}/close`,{method:"POST"})}finally{w.close()}}}),w.showModal()}l.addEventListener("click",t=>{let e=t.target;if(e.classList.contains("row-select")){let C=e.closest("tr[data-id]");if(!C)return;let I=C.dataset.id;e.checked?g.add(I):g.delete(I),f(S());return}let n=e.closest("td");if(n&&n.querySelector(".row-select"))return;let u=e.closest("tr[data-id]");if(!u)return;let d=u.dataset.id,H=T.sessions.get(d);H&&h(H)}),$.addEventListener("change",()=>{let t=S().filter(e=>e.status!=="closed");if($.checked)for(let e of t)g.add(e.sessionId);else for(let e of t)g.delete(e.sessionId);s()}),x.addEventListener("click",()=>{g.clear(),s()}),v.addEventListener("click",async()=>{let t=[...g];if(t.length===0||!confirm(`\u5173\u95ED\u9009\u4E2D\u7684 ${t.length} \u4E2A\u4F1A\u8BDD\uFF1F`))return;v.disabled=!0,x.disabled=!0;let e=v.textContent,n=0,u=0,d=[];v.textContent=`\u5173\u95ED\u4E2D 0/${t.length}...`;let H=[...t];async function C(){for(;H.length;){let I=H.shift();try{let q=await fetch(`/api/sessions/${encodeURIComponent(I)}/close`,{method:"POST"}),O=null;try{O=await q.json()}catch{}if(!q.ok||O?.ok===!1){u+=1;let W=O?.error??`HTTP ${q.status}`;d.push(`${I.slice(0,12)}\u2026: ${W}`)}}catch(q){u+=1,d.push(`${I.slice(0,12)}\u2026: ${q?.message??q}`)}finally{n+=1,v.textContent=`\u5173\u95ED\u4E2D ${n}/${t.length}...`}}}if(await Promise.all(Array.from({length:Math.min(6,t.length)},()=>C())),v.textContent=e,v.disabled=!1,x.disabled=!1,g.clear(),s(),u>0){let I=d.slice(0,3).join(`
68
+ `),q=d.length>3?`
69
+ ... +${d.length-3} \u4E2A`:"";alert(`\u5173\u95ED\u5B8C\u6210\uFF1A\u6210\u529F ${t.length-u} / \u5931\u8D25 ${u}
70
+ ${I}${q}`)}}),M.querySelectorAll("th[data-sort]").forEach(t=>{t.addEventListener("click",()=>{let e=t.dataset.sort;E===e?i=i==="asc"?"desc":"asc":(E=e,i=e==="spawnedAt"||e==="lastMessageAt"?"desc":"asc"),s()})}),b.addEventListener("input",s),T.on(s),s()}var Z=`
71
71
  <form id="sched-filters" class="filters">
72
72
  <input type="search" name="q" placeholder="search name / prompt / workingDir" />
73
73
  <select name="kind">
@@ -85,19 +85,19 @@ ${I}${x}`)}}),M.querySelectorAll("th[data-sort]").forEach(t=>{t.addEventListener
85
85
  </tr></thead>
86
86
  <tbody id="schedules-tbody"></tbody>
87
87
  </table>
88
- `;function j(r){return r.replace(/[&<>"']/g,l=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[l])}function J(r){if(!r)return"\u2014";try{return new Date(r).toLocaleString()}catch{return r}}function G(r){r.innerHTML=Z;let l=r.querySelector("#schedules-tbody"),h=r.querySelector("#sched-filters");function w(){let o=new FormData(h),L=(o.get("q")??"").toLowerCase(),v=o.get("kind"),A=!!o.get("enabled");return[...T.schedules.values()].filter(M=>!v||M.parsed?.kind===v).filter(M=>!A||M.enabled).filter(M=>!L||JSON.stringify(M).toLowerCase().includes(L)).sort((M,m)=>{if(M.enabled!==m.enabled)return M.enabled?-1:1;let S=M.nextRunAt?Date.parse(M.nextRunAt):1/0,s=m.nextRunAt?Date.parse(m.nextRunAt):1/0;return S-s})}function $(){l.innerHTML=w().map(o=>`<tr data-id="${j(o.id)}">
89
- <td>${j(o.name??o.id)}</td>
90
- <td>${j(o.botName??o.larkAppId??"-")}</td>
91
- <td><code>${j(o.parsed?.display??"?")}</code></td>
92
- <td>${J(o.nextRunAt)}</td>
93
- <td>${J(o.lastRunAt)} ${o.lastStatus==="error"?"\u26A0\uFE0F":""}</td>
94
- <td>${o.repeat?`${o.repeat.completed}/${o.repeat.times??"\u221E"}`:"\u2014"}</td>
95
- <td>${o.enabled?"\u2713":"\u2717"}</td>
88
+ `;function j(r){return r.replace(/[&<>"']/g,l=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[l])}function J(r){if(!r)return"\u2014";try{return new Date(r).toLocaleString()}catch{return r}}function G(r){r.innerHTML=Z;let l=r.querySelector("#schedules-tbody"),b=r.querySelector("#sched-filters");function w(){let a=new FormData(b),L=(a.get("q")??"").toLowerCase(),v=a.get("kind"),x=!!a.get("enabled");return[...T.schedules.values()].filter(M=>!v||M.parsed?.kind===v).filter(M=>!x||M.enabled).filter(M=>!L||JSON.stringify(M).toLowerCase().includes(L)).sort((M,g)=>{if(M.enabled!==g.enabled)return M.enabled?-1:1;let E=M.nextRunAt?Date.parse(M.nextRunAt):1/0,i=g.nextRunAt?Date.parse(g.nextRunAt):1/0;return E-i})}function $(){l.innerHTML=w().map(a=>`<tr data-id="${j(a.id)}">
89
+ <td>${j(a.name??a.id)}</td>
90
+ <td>${j(a.botName??a.larkAppId??"-")}</td>
91
+ <td><code>${j(a.parsed?.display??"?")}</code></td>
92
+ <td>${J(a.nextRunAt)}</td>
93
+ <td>${J(a.lastRunAt)} ${a.lastStatus==="error"?"\u26A0\uFE0F":""}</td>
94
+ <td>${a.repeat?`${a.repeat.completed}/${a.repeat.times??"\u221E"}`:"\u2014"}</td>
95
+ <td>${a.enabled?"\u2713":"\u2717"}</td>
96
96
  <td class="actions-cell">
97
97
  <button data-op="run" type="button">Run now</button>
98
- ${o.enabled?'<button data-op="pause" type="button">Pause</button>':'<button data-op="resume" type="button">Resume</button>'}
98
+ ${a.enabled?'<button data-op="pause" type="button">Pause</button>':'<button data-op="resume" type="button">Resume</button>'}
99
99
  </td>
100
- </tr>`).join("")||'<tr><td colspan="8" class="empty">No schedules.</td></tr>'}l.addEventListener("click",async o=>{let L=o.target.closest("button[data-op]");if(!L)return;let v=L.closest("tr[data-id]");if(!v)return;let A=v.dataset.id,M=L.dataset.op;L.disabled=!0;let m=L.textContent;L.textContent="...";try{let S=await fetch(`/api/schedules/${encodeURIComponent(A)}/${M}`,{method:"POST"}),s=await S.json().catch(()=>({}));(!S.ok||s.ok===!1)&&alert(`Failed: ${S.status} ${s?.error??""}`.trim())}catch(S){alert("Network error: "+S)}finally{L.disabled=!1,L.textContent=m}}),h.addEventListener("input",$),T.on($),$()}var q={chats:[],bots:[]},tt=`
100
+ </tr>`).join("")||'<tr><td colspan="8" class="empty">No schedules.</td></tr>'}l.addEventListener("click",async a=>{let L=a.target.closest("button[data-op]");if(!L)return;let v=L.closest("tr[data-id]");if(!v)return;let x=v.dataset.id,M=L.dataset.op;L.disabled=!0;let g=L.textContent;L.textContent="...";try{let E=await fetch(`/api/schedules/${encodeURIComponent(x)}/${M}`,{method:"POST"}),i=await E.json().catch(()=>({}));(!E.ok||i.ok===!1)&&alert(`Failed: ${E.status} ${i?.error??""}`.trim())}catch(E){alert("Network error: "+E)}finally{L.disabled=!1,L.textContent=g}}),b.addEventListener("input",$),T.on($),$()}var A={chats:[],bots:[]},tt=`
101
101
  <form id="g-filters" class="filters">
102
102
  <input type="search" name="q" placeholder="search chat name / id / owner" />
103
103
  <label><input type="checkbox" name="missing"> missing-bot only</label>
@@ -109,7 +109,7 @@ ${I}${x}`)}}),M.querySelectorAll("th[data-sort]").forEach(t=>{t.addEventListener
109
109
  <tbody id="g-body"></tbody>
110
110
  </table>
111
111
  <dialog id="g-drawer"></dialog>
112
- `;function f(r){return r.replace(/[&<>"']/g,l=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[l])}async function B(){q=await(await fetch("/api/groups")).json()}async function z(r){r.innerHTML=tt;let l=r.querySelector("#g-head"),h=r.querySelector("#g-body"),w=r.querySelector("#g-filters"),$=r.querySelector("#g-refresh"),o=r.querySelector("#g-drawer");$.onclick=async()=>{$.disabled=!0;try{await B(),m()}finally{$.disabled=!1}};let L=r.querySelector("#g-create");L.onclick=()=>v(),await B();function v(){let s=q.bots;if(s.length===0){alert("No bots online. Restart the daemon first.");return}o.innerHTML=`
112
+ `;function m(r){return r.replace(/[&<>"']/g,l=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[l])}async function N(){A=await(await fetch("/api/groups")).json()}async function et(){return(await fetch("/api/groups")).json()}function nt(r,l){if(l.size===0)return!0;let b=r?.memberBots??[];for(let w of l)if(!b.some($=>$.larkAppId===w&&$.inChat))return!1;return!0}async function z(r){r.innerHTML=tt;let l=r.querySelector("#g-head"),b=r.querySelector("#g-body"),w=r.querySelector("#g-filters"),$=r.querySelector("#g-refresh"),a=r.querySelector("#g-drawer");$.onclick=async()=>{$.disabled=!0;try{await N(),g()}finally{$.disabled=!1}};let L=r.querySelector("#g-create");L.onclick=()=>v(),await N();function v(){let i=A.bots;if(i.length===0){alert("No bots online. Restart the daemon first.");return}a.innerHTML=`
113
113
  <article>
114
114
  <header><h3>Create new group</h3></header>
115
115
  <p>Pick bots to invite. The dashboard auto-selects an online daemon as the chat creator/owner; the rest are added as members in the same call.</p>
@@ -120,10 +120,10 @@ ${I}${x}`)}}),M.querySelectorAll("th[data-sort]").forEach(t=>{t.addEventListener
120
120
  </label>
121
121
  <fieldset>
122
122
  <legend>Bots</legend>
123
- ${s.map(b=>`
123
+ ${i.map(o=>`
124
124
  <label class="checkbox-row">
125
- <input type="checkbox" name="bot" value="${f(b.larkAppId)}">
126
- ${f(b.botName??b.larkAppId)} <small>(${f(b.larkAppId)})</small>
125
+ <input type="checkbox" name="bot" value="${m(o.larkAppId)}">
126
+ ${m(o.botName??o.larkAppId)} <small>(${m(o.larkAppId)})</small>
127
127
  </label>
128
128
  `).join("")}
129
129
  </fieldset>
@@ -132,40 +132,40 @@ ${I}${x}`)}}),M.querySelectorAll("th[data-sort]").forEach(t=>{t.addEventListener
132
132
  <button type="button" id="g-create-cancel">Cancel</button>
133
133
  </div>
134
134
  </form>
135
- </article>`,o.showModal(),o.querySelector("#g-create-cancel").onclick=()=>o.close(),o.querySelector("#g-createform").onsubmit=async b=>{b.preventDefault();let E=new FormData(b.target),c=(E.get("name")??"").trim(),d=E.getAll("bot");if(d.length===0){alert("Pick at least one bot.");return}let p=b.target.querySelector("button[type=submit]");p&&(p.disabled=!0,p.textContent="Creating...");try{let a=await fetch("/api/groups/create",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({name:c||void 0,larkAppIds:d})}),y=await a.json();y.ok&&y.chatId?(A(y),B().then(m).catch(()=>{})):(alert(`Failed: ${y.error??a.status}`),o.close())}catch(a){alert("Network error: "+a),o.close()}}}function A(s){let b=String(s.chatId),E=`https://applink.feishu.cn/client/chat/open?openChatId=${encodeURIComponent(b)}`,c=s.invalidBotIds??[],d=s.invalidUserIds??[],p=s.autoInvitedOpenId,a=!!s.autoInviteRejected,y=s.ownerTransferredTo,g=s.transferError,t=s.notifyMessageId,e=s.notifyError,n;if(p){let i=y?"<br><small>\u7FA4\u4E3B\u5DF2\u4ECE\u673A\u5668\u4EBA\u8F6C\u8BA9\u7ED9\u4F60\u3002</small>":g?`<br><small class="hint-warn-inline">\u26A0 \u81EA\u52A8\u8F6C\u8BA9\u7FA4\u4E3B\u5931\u8D25\uFF08${f(g)}\uFF09\uFF0C\u4F60\u73B0\u5728\u662F\u6210\u5458\u4F46\u7FA4\u4E3B\u4ECD\u662F\u673A\u5668\u4EBA\u3002</small>`:"",C=t?`<br><small>\u673A\u5668\u4EBA\u5DF2\u5728\u7FA4\u91CC @ \u4E86\u4F60\uFF08\u6D88\u606F id <code>${f(t)}</code>\uFF09\uFF0C\u770B\u98DE\u4E66\u901A\u77E5\u5C31\u80FD\u8FDB\u7FA4\u3002</small>`:e?`<br><small class="hint-warn-inline">\u26A0 \u81EA\u52A8 @ \u901A\u77E5\u5931\u8D25\uFF08${f(e)}\uFF09\uFF0C\u65B0\u7FA4\u53EF\u80FD\u4E0D\u4F1A\u4E3B\u52A8\u51FA\u73B0\u5728\u4F60\u4FA7\u8FB9\u680F\uFF0C\u5EFA\u8BAE\u4ECE\u4E0B\u9762\u6309\u94AE\u8DF3\u8FDB\u53BB\u3002</small>`:"";n=`<p class="hint-ok">\u5DF2\u81EA\u52A8\u9080\u8BF7\u4F60\uFF08<code>${f(p)}</code>\uFF09\u4F5C\u4E3A\u6210\u5458\u3002${i}${C}</p>`}else a?n='<p class="hint-warn">\u98DE\u4E66\u62D2\u7EDD\u4E86\u81EA\u52A8\u9080\u8BF7\uFF08\u4F60\u7684 open_id \u5728\u521B\u5EFA\u8005 bot \u7684 scope \u4E0B\u4E0D\u53EF\u7528\uFF09\u3002<strong>\u4F60\u76EE\u524D\u4E0D\u662F\u65B0\u7FA4\u6210\u5458</strong>\uFF0C\u9700\u8981\u8BA9\u7FA4\u91CC\u7684\u67D0\u4E2A\u673A\u5668\u4EBA\u624B\u52A8\u628A\u4F60\u52A0\u8FDB\u6765\u3002</p>':n='<p class="hint-warn">\u6CA1\u5728 dashboard \u7F13\u5B58\u91CC\u627E\u5230 ownerOpenId\uFF0C<strong>\u6CA1\u6709\u81EA\u52A8\u9080\u8BF7\u4F60</strong>\u3002\u70B9\u5F00\u4E0B\u9762\u94FE\u63A5\u524D\uFF0C\u5148\u8BA9\u7FA4\u91CC\u4EFB\u4E00\u673A\u5668\u4EBA\u624B\u52A8\u628A\u4F60\u52A0\u8FDB\u53BB\u3002</p>';let u=[c.length?`<li>\u65E0\u6548 bot id: <code>${c.map(f).join(", ")}</code></li>`:"",d.length?`<li>\u65E0\u6548\u7528\u6237 open_id: <code>${d.map(f).join(", ")}</code></li>`:""].filter(Boolean).join("");o.innerHTML=`
135
+ </article>`,a.showModal(),a.querySelector("#g-create-cancel").onclick=()=>a.close(),a.querySelector("#g-createform").onsubmit=async o=>{o.preventDefault();let c=new FormData(o.target),p=(c.get("name")??"").trim(),s=c.getAll("bot");if(s.length===0){alert("Pick at least one bot.");return}let f=o.target.querySelector("button[type=submit]");f&&(f.disabled=!0,f.textContent="Creating...");try{let h=await fetch("/api/groups/create",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({name:p||void 0,larkAppIds:s})}),t=await h.json();if(t.ok&&t.chatId){x(t);let e=Array.isArray(t.invalidBotIds)?t.invalidBotIds:[],n=s.filter(d=>!e.includes(d)),u=new Set(n);typeof t.creator=="string"&&t.creator&&u.add(t.creator),y(t.chatId,p||t.chatId,n,t.creator),g(),S(t.chatId,u).catch(()=>{})}else alert(`Failed: ${t.error??h.status}`),a.close()}catch(h){alert("Network error: "+h),a.close()}};function y(o,c,p,s){let f=new Set(p);s&&f.add(s);let h=A.bots.map(e=>({larkAppId:e.larkAppId,botName:e.botName,inChat:f.has(e.larkAppId),oncallChat:null})),t={chatId:o,name:c,ownerId:s??null,memberBots:h};A.chats=[t,...A.chats.filter(e=>e.chatId!==o)]}async function S(o,c){let p=[600,1200,1200,1200,1200,1200];for(let s of p){await new Promise(t=>setTimeout(t,s));let f;try{f=await et()}catch{continue}let h=(f.chats??[]).find(t=>t.chatId===o);if(h&&nt(h,c)){A=f,g();return}}}}function x(i){let y=String(i.chatId),S=`https://applink.feishu.cn/client/chat/open?openChatId=${encodeURIComponent(y)}`,o=i.invalidBotIds??[],c=i.invalidUserIds??[],p=i.autoInvitedOpenId,s=!!i.autoInviteRejected,f=i.ownerTransferredTo,h=i.transferError,t=i.notifyMessageId,e=i.notifyError,n;if(p){let d=f?"<br><small>\u7FA4\u4E3B\u5DF2\u4ECE\u673A\u5668\u4EBA\u8F6C\u8BA9\u7ED9\u4F60\u3002</small>":h?`<br><small class="hint-warn-inline">\u26A0 \u81EA\u52A8\u8F6C\u8BA9\u7FA4\u4E3B\u5931\u8D25\uFF08${m(h)}\uFF09\uFF0C\u4F60\u73B0\u5728\u662F\u6210\u5458\u4F46\u7FA4\u4E3B\u4ECD\u662F\u673A\u5668\u4EBA\u3002</small>`:"",H=t?`<br><small>\u673A\u5668\u4EBA\u5DF2\u5728\u7FA4\u91CC @ \u4E86\u4F60\uFF08\u6D88\u606F id <code>${m(t)}</code>\uFF09\uFF0C\u770B\u98DE\u4E66\u901A\u77E5\u5C31\u80FD\u8FDB\u7FA4\u3002</small>`:e?`<br><small class="hint-warn-inline">\u26A0 \u81EA\u52A8 @ \u901A\u77E5\u5931\u8D25\uFF08${m(e)}\uFF09\uFF0C\u65B0\u7FA4\u53EF\u80FD\u4E0D\u4F1A\u4E3B\u52A8\u51FA\u73B0\u5728\u4F60\u4FA7\u8FB9\u680F\uFF0C\u5EFA\u8BAE\u4ECE\u4E0B\u9762\u6309\u94AE\u8DF3\u8FDB\u53BB\u3002</small>`:"";n=`<p class="hint-ok">\u5DF2\u81EA\u52A8\u9080\u8BF7\u4F60\uFF08<code>${m(p)}</code>\uFF09\u4F5C\u4E3A\u6210\u5458\u3002${d}${H}</p>`}else s?n='<p class="hint-warn">\u98DE\u4E66\u62D2\u7EDD\u4E86\u81EA\u52A8\u9080\u8BF7\uFF08\u4F60\u7684 open_id \u5728\u521B\u5EFA\u8005 bot \u7684 scope \u4E0B\u4E0D\u53EF\u7528\uFF09\u3002<strong>\u4F60\u76EE\u524D\u4E0D\u662F\u65B0\u7FA4\u6210\u5458</strong>\uFF0C\u9700\u8981\u8BA9\u7FA4\u91CC\u7684\u67D0\u4E2A\u673A\u5668\u4EBA\u624B\u52A8\u628A\u4F60\u52A0\u8FDB\u6765\u3002</p>':n='<p class="hint-warn">\u6CA1\u5728 dashboard \u7F13\u5B58\u91CC\u627E\u5230 ownerOpenId\uFF0C<strong>\u6CA1\u6709\u81EA\u52A8\u9080\u8BF7\u4F60</strong>\u3002\u70B9\u5F00\u4E0B\u9762\u94FE\u63A5\u524D\uFF0C\u5148\u8BA9\u7FA4\u91CC\u4EFB\u4E00\u673A\u5668\u4EBA\u624B\u52A8\u628A\u4F60\u52A0\u8FDB\u53BB\u3002</p>';let u=[o.length?`<li>\u65E0\u6548 bot id: <code>${o.map(m).join(", ")}</code></li>`:"",c.length?`<li>\u65E0\u6548\u7528\u6237 open_id: <code>${c.map(m).join(", ")}</code></li>`:""].filter(Boolean).join("");a.innerHTML=`
136
136
  <article>
137
137
  <header><h3>\u7FA4\u521B\u5EFA\u6210\u529F</h3></header>
138
- <p><b>chatId:</b> <code>${f(b)}</code> <button type="button" data-copy="${f(b)}">copy</button></p>
139
- <p><b>\u521B\u5EFA\u8005:</b> <code>${f(s.creator??"?")}</code></p>
138
+ <p><b>chatId:</b> <code>${m(y)}</code> <button type="button" data-copy="${m(y)}">copy</button></p>
139
+ <p><b>\u521B\u5EFA\u8005:</b> <code>${m(i.creator??"?")}</code></p>
140
140
  ${n}
141
141
  ${u?`<ul>${u}</ul>`:""}
142
142
  <div class="actions">
143
- <a class="btn-link primary" href="${E}" target="_blank" rel="noopener">\u2197 \u6253\u5F00\u65B0\u7FA4</a>
143
+ <a class="btn-link primary" href="${S}" target="_blank" rel="noopener">\u2197 \u6253\u5F00\u65B0\u7FA4</a>
144
144
  <button type="button" id="g-create-close">\u5173\u95ED</button>
145
145
  </div>
146
- </article>`,o.querySelectorAll("[data-copy]").forEach(i=>{i.onclick=()=>{navigator.clipboard.writeText(i.dataset.copy??""),i.textContent="copied",setTimeout(()=>{i.textContent="copy"},800)}}),o.querySelector("#g-create-close").onclick=()=>o.close()}function M(){l.innerHTML=`<tr>
146
+ </article>`,a.querySelectorAll("[data-copy]").forEach(d=>{d.onclick=()=>{navigator.clipboard.writeText(d.dataset.copy??""),d.textContent="copied",setTimeout(()=>{d.textContent="copy"},800)}}),a.querySelector("#g-create-close").onclick=()=>a.close()}function M(){l.innerHTML=`<tr>
147
147
  <th>chat</th>
148
- ${q.bots.map(s=>`<th>${f(s.botName??s.larkAppId)}</th>`).join("")}
148
+ ${A.bots.map(i=>`<th>${m(i.botName??i.larkAppId)}</th>`).join("")}
149
149
  <th>actions</th>
150
- </tr>`}function m(){M();let s=new FormData(w),b=(s.get("q")??"").toLowerCase(),E=!!s.get("missing"),c=q.chats.filter(d=>!b||(d.name??"").toLowerCase().includes(b)||d.chatId.toLowerCase().includes(b)||(d.ownerId??"").toLowerCase().includes(b)).filter(d=>!E||d.memberBots.some(p=>!p.inChat));if(c.length===0){h.innerHTML=`<tr><td colspan="${q.bots.length+2}" class="empty">No chats match the filter.</td></tr>`;return}h.innerHTML=c.map(d=>`<tr data-chat="${f(d.chatId)}">
150
+ </tr>`}function g(){M();let i=new FormData(w),y=(i.get("q")??"").toLowerCase(),S=!!i.get("missing"),o=A.chats.filter(c=>!y||(c.name??"").toLowerCase().includes(y)||c.chatId.toLowerCase().includes(y)||(c.ownerId??"").toLowerCase().includes(y)).filter(c=>!S||c.memberBots.some(p=>!p.inChat));if(o.length===0){b.innerHTML=`<tr><td colspan="${A.bots.length+2}" class="empty">No chats match the filter.</td></tr>`;return}b.innerHTML=o.map(c=>`<tr data-chat="${m(c.chatId)}">
151
151
  <td>
152
- <strong>${f(d.name??d.chatId)}</strong><br>
153
- <small><code>${f(d.chatId)}</code></small>
152
+ <strong>${m(c.name??c.chatId)}</strong><br>
153
+ <small><code>${m(c.chatId)}</code></small>
154
154
  </td>
155
- ${q.bots.map(p=>{let a=d.memberBots.find(t=>t.larkAppId===p.larkAppId),y=a?a.error?"!":a.inChat?"\u2713":"\u2717":"?";return`<td class="${a?a.error?"cell-error":a.inChat?"cell-in":"cell-out":"cell-unknown"}" title="${f(a?.error??"")}">${y}</td>`}).join("")}
155
+ ${A.bots.map(p=>{let s=c.memberBots.find(t=>t.larkAppId===p.larkAppId),f=s?s.error?"!":s.inChat?"\u2713":"\u2717":"?";return`<td class="${s?s.error?"cell-error":s.inChat?"cell-in":"cell-out":"cell-unknown"}" title="${m(s?.error??"")}">${f}</td>`}).join("")}
156
156
  <td>
157
157
  <button class="add-bots" type="button">Add bots</button>
158
158
  <button class="manage-chat" type="button">Manage</button>
159
159
  </td>
160
- </tr>`).join("")}m(),h.addEventListener("click",async s=>{let b=s.target.closest("button.add-bots");if(!b)return;let c=b.closest("tr[data-chat]").dataset.chat,d=q.chats.find(a=>a.chatId===c);if(!d)return;let p=d.memberBots.filter(a=>!a.inChat);if(!p.length){alert("All configured bots are already in this chat.");return}o.innerHTML=`
160
+ </tr>`).join("")}g(),b.addEventListener("click",async i=>{let y=i.target.closest("button.add-bots");if(!y)return;let o=y.closest("tr[data-chat]").dataset.chat,c=A.chats.find(s=>s.chatId===o);if(!c)return;let p=c.memberBots.filter(s=>!s.inChat);if(!p.length){alert("All configured bots are already in this chat.");return}a.innerHTML=`
161
161
  <article>
162
- <header><h3>Add bots to ${f(d.name??d.chatId)}</h3></header>
162
+ <header><h3>Add bots to ${m(c.name??c.chatId)}</h3></header>
163
163
  <p>Select bots to add. The dashboard will pick a bot that's already in the chat as the proxy.</p>
164
164
  <form id="g-addform">
165
- ${p.map(a=>`
165
+ ${p.map(s=>`
166
166
  <label class="checkbox-row">
167
- <input type="checkbox" name="bot" value="${f(a.larkAppId)}">
168
- ${f(a.botName??a.larkAppId)} <small>(${f(a.larkAppId)})</small>
167
+ <input type="checkbox" name="bot" value="${m(s.larkAppId)}">
168
+ ${m(s.botName??s.larkAppId)} <small>(${m(s.larkAppId)})</small>
169
169
  </label>
170
170
  `).join("")}
171
171
  <div class="actions">
@@ -173,26 +173,26 @@ ${I}${x}`)}}),M.querySelectorAll("th[data-sort]").forEach(t=>{t.addEventListener
173
173
  <button type="button" id="g-cancel">Cancel</button>
174
174
  </div>
175
175
  </form>
176
- </article>`,o.showModal(),o.querySelector("#g-cancel").onclick=()=>o.close(),o.querySelector("#g-addform").onsubmit=async a=>{a.preventDefault();let g=new FormData(a.target).getAll("bot");if(g.length===0){alert("Pick at least one bot.");return}try{let e=await(await fetch(`/api/groups/${encodeURIComponent(c)}/add-bots`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({larkAppIds:g})})).json();if(e.error==="no_proxy_bot")alert("No bot is currently in this chat \u2014 add one manually in Feishu first, then retry.");else if(e.result){let n=e.result.map(u=>`${u.id}: ${u.ok?"OK":`failed (${u.error??"unknown"})`}`).join(`
177
- `);alert(n),await B(),m()}else alert(`Unexpected response: ${JSON.stringify(e)}`)}catch(t){alert("Network error: "+t)}finally{o.close()}}}),h.addEventListener("click",async s=>{let b=s.target.closest("button.manage-chat");if(!b)return;let c=b.closest("tr[data-chat]").dataset.chat,d=q.chats.find(p=>p.chatId===c);d&&S(d)});function S(s){let b=s.memberBots.filter(c=>c.inChat),E=typeof s.ownerId=="string"?s.ownerId:"";o.innerHTML=`
176
+ </article>`,a.showModal(),a.querySelector("#g-cancel").onclick=()=>a.close(),a.querySelector("#g-addform").onsubmit=async s=>{s.preventDefault();let h=new FormData(s.target).getAll("bot");if(h.length===0){alert("Pick at least one bot.");return}try{let e=await(await fetch(`/api/groups/${encodeURIComponent(o)}/add-bots`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({larkAppIds:h})})).json();if(e.error==="no_proxy_bot")alert("No bot is currently in this chat \u2014 add one manually in Feishu first, then retry.");else if(e.result){let n=e.result.map(u=>`${u.id}: ${u.ok?"OK":`failed (${u.error??"unknown"})`}`).join(`
177
+ `);alert(n),await N(),g()}else alert(`Unexpected response: ${JSON.stringify(e)}`)}catch(t){alert("Network error: "+t)}finally{a.close()}}}),b.addEventListener("click",async i=>{let y=i.target.closest("button.manage-chat");if(!y)return;let o=y.closest("tr[data-chat]").dataset.chat,c=A.chats.find(p=>p.chatId===o);c&&E(c)});function E(i){let y=i.memberBots.filter(o=>o.inChat),S=typeof i.ownerId=="string"?i.ownerId:"";a.innerHTML=`
178
178
  <article>
179
- <header><h3>Manage ${f(s.name??s.chatId)}</h3></header>
180
- <p><b>chatId:</b> <code>${f(s.chatId)}</code></p>
181
- <p><b>owner:</b> <code>${f(s.ownerId??"(unknown)")}</code></p>
179
+ <header><h3>Manage ${m(i.name??i.chatId)}</h3></header>
180
+ <p><b>chatId:</b> <code>${m(i.chatId)}</code></p>
181
+ <p><b>owner:</b> <code>${m(i.ownerId??"(unknown)")}</code></p>
182
182
 
183
183
  <fieldset>
184
184
  <legend>Oncall \u6A21\u5F0F</legend>
185
185
  <p><small>\u5F00\u542F\u540E\uFF1A\u7FA4\u5185\u4EFB\u4F55\u6210\u5458\u90FD\u80FD @ \u673A\u5668\u4EBA\u63D0\u95EE\uFF0C\u65B0\u8BDD\u9898\u76F4\u63A5\u7528\u7ED1\u5B9A\u76EE\u5F55\u542F\u52A8 CLI\uFF1B\u4EC5 allowedUsers \u4ECD\u53EF\u6267\u884C /cd /restart \u7B49\u547D\u4EE4\u3002</small></p>
186
- ${b.length===0?'<p class="empty">\u6CA1\u6709\u673A\u5668\u4EBA\u5728\u7FA4\u91CC</p>':b.map(c=>{let d=!!c.oncallChat,p=c.oncallChat?.workingDir??"";return`
187
- <div class="oncall-row" data-bot="${f(c.larkAppId)}">
186
+ ${y.length===0?'<p class="empty">\u6CA1\u6709\u673A\u5668\u4EBA\u5728\u7FA4\u91CC</p>':y.map(o=>{let c=!!o.oncallChat,p=o.oncallChat?.workingDir??"";return`
187
+ <div class="oncall-row" data-bot="${m(o.larkAppId)}">
188
188
  <label class="checkbox-row">
189
- <input type="checkbox" data-action="toggle" ${d?"checked":""}>
190
- <strong>${f(c.botName??c.larkAppId)}</strong>
191
- <small>(${f(c.larkAppId)})</small>
189
+ <input type="checkbox" data-action="toggle" ${c?"checked":""}>
190
+ <strong>${m(o.botName??o.larkAppId)}</strong>
191
+ <small>(${m(o.larkAppId)})</small>
192
192
  </label>
193
193
  <div class="oncall-row-body">
194
194
  <input type="text" data-input="workingDir" placeholder="e.g. /root/iserver/botmux"
195
- value="${f(p)}" ${d?"":"disabled"}>
195
+ value="${m(p)}" ${c?"":"disabled"}>
196
196
  <button type="button" data-action="save">Save</button>
197
197
  <span class="oncall-status" data-status></span>
198
198
  </div>
@@ -202,26 +202,26 @@ ${I}${x}`)}}),M.querySelectorAll("th[data-sort]").forEach(t=>{t.addEventListener
202
202
 
203
203
  <fieldset>
204
204
  <legend>\u9009\u62E9\u673A\u5668\u4EBA\u9000\u51FA\u7FA4\u804A</legend>
205
- ${b.length===0?'<p class="empty">\u6CA1\u6709\u673A\u5668\u4EBA\u5728\u7FA4\u91CC</p>':b.map(c=>`
205
+ ${y.length===0?'<p class="empty">\u6CA1\u6709\u673A\u5668\u4EBA\u5728\u7FA4\u91CC</p>':y.map(o=>`
206
206
  <label class="checkbox-row">
207
- <input type="checkbox" name="leave-bot" value="${f(c.larkAppId)}">
208
- ${f(c.botName??c.larkAppId)}
209
- <small>${c.larkAppId===E?"\xB7 \u7FA4\u4E3B":""}</small>
207
+ <input type="checkbox" name="leave-bot" value="${m(o.larkAppId)}">
208
+ ${m(o.botName??o.larkAppId)}
209
+ <small>${o.larkAppId===S?"\xB7 \u7FA4\u4E3B":""}</small>
210
210
  </label>
211
211
  `).join("")}
212
212
  </fieldset>
213
213
 
214
214
  <div class="actions">
215
- <button id="g-leave-btn" type="button" ${b.length===0?"disabled":""}>\u9009\u4E2D\u673A\u5668\u4EBA\u9000\u51FA\u7FA4\u804A</button>
216
- <button id="g-disband-btn" type="button" class="contrast" ${b.length===0?"disabled":""}>\u89E3\u6563\u7FA4\u804A</button>
215
+ <button id="g-leave-btn" type="button" ${y.length===0?"disabled":""}>\u9009\u4E2D\u673A\u5668\u4EBA\u9000\u51FA\u7FA4\u804A</button>
216
+ <button id="g-disband-btn" type="button" class="contrast" ${y.length===0?"disabled":""}>\u89E3\u6563\u7FA4\u804A</button>
217
217
  </div>
218
218
  <p class="hint-warn"><small>\u89E3\u6563\u7FA4\u804A\u4EC5\u5F53\u67D0\u4E2A\u5728\u7FA4\u673A\u5668\u4EBA\u662F\u7FA4\u4E3B\u65F6\u624D\u4F1A\u6210\u529F\u3002\u5426\u5219\u98DE\u4E66\u4F1A\u8FD4\u56DE\u9519\u8BEF\uFF0C\u5EFA\u8BAE\u6539\u7528\u300C\u9000\u51FA\u7FA4\u804A\u300D\u3002</small></p>
219
219
  <form method="dialog"><button>\u5173\u95ED</button></form>
220
- </article>`,o.showModal(),o.querySelectorAll(".oncall-row").forEach(c=>{let d=c.dataset.bot,p=c.querySelector("input[data-action=toggle]"),a=c.querySelector("input[data-input=workingDir]"),y=c.querySelector("button[data-action=save]"),g=c.querySelector("[data-status]");p.addEventListener("change",()=>{a.disabled=!p.checked,p.checked&&a.focus()}),y.addEventListener("click",async()=>{g.textContent="",g.className="oncall-status";let t=p.checked,e=a.value.trim();if(t&&!e){g.textContent="\u8BF7\u586B\u5DE5\u4F5C\u76EE\u5F55",g.classList.add("hint-warn-inline");return}y.disabled=!0;try{let n=`/api/groups/${encodeURIComponent(s.chatId)}/oncall/${encodeURIComponent(d)}`,u=t?await fetch(n,{method:"PUT",headers:{"content-type":"application/json"},body:JSON.stringify({workingDir:e})}):await fetch(n,{method:"DELETE"}),i=await u.json().catch(()=>({}));if(u.ok&&i.ok){g.textContent=t?`\u2713 \u5DF2\u7ED1\u5B9A \u2192 ${i.resolvedPath??e}`:"\u2713 \u5DF2\u89E3\u7ED1",g.classList.add("hint-ok");try{await B(),m()}catch{}}else g.textContent=`\u2717 ${i.error??u.status}`,g.classList.add("hint-warn-inline")}catch(n){g.textContent=`\u2717 ${n?.message??n}`,g.classList.add("hint-warn-inline")}finally{y.disabled=!1}})}),o.querySelector("#g-leave-btn").onclick=async()=>{let c=[...o.querySelectorAll("input[name=leave-bot]:checked")].map(d=>d.value);if(c.length===0){alert("\u81F3\u5C11\u9009\u4E00\u4E2A\u673A\u5668\u4EBA");return}if(confirm(`\u786E\u5B9A\u8BA9 ${c.length} \u4E2A\u673A\u5668\u4EBA\u9000\u51FA\u7FA4\u804A\uFF1F\u8BE5 bot \u5728\u6B64\u7FA4\u7684\u4F1A\u8BDD\u4F1A\u4E00\u5E76\u5173\u95ED\u3002`))try{let p=await(await fetch(`/api/groups/${encodeURIComponent(s.chatId)}/leave`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({larkAppIds:c})})).json(),a=(p.result??[]).map(y=>{if(!y.ok)return`${y.larkAppId}: \u5931\u8D25 (${y.error??"unknown"})`;let g=y.closedSessions??[],t=g.filter(u=>!u.ok).length,e=g.length-t,n=g.length===0?"":t===0?`\uFF08\u5173\u95ED ${e} \u4E2A\u4F1A\u8BDD\uFF09`:`\uFF08\u5173\u95ED ${e} \u4E2A\uFF0C${t} \u4E2A\u5931\u8D25\uFF09`;return`${y.larkAppId}: OK${n}`}).join(`
221
- `);alert(a||`Unexpected: ${JSON.stringify(p)}`),await B(),m()}catch(d){alert("Network error: "+d)}finally{o.close()}},o.querySelector("#g-disband-btn").onclick=async()=>{if(b.length===0||!confirm(`\u786E\u5B9A\u89E3\u6563\u7FA4\u804A\u300C${s.name??s.chatId}\u300D\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\uFF0C\u672C\u7FA4\u6240\u6709\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E5F\u4F1A\u4E00\u5E76\u5173\u95ED\u3002`))return;let c=[...b].sort((p,a)=>(a.larkAppId===E?1:0)-(p.larkAppId===E?1:0)),d=[];for(let p of c)try{let a=await fetch(`/api/groups/${encodeURIComponent(s.chatId)}/disband`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({larkAppId:p.larkAppId})}),y=await a.json();if(y.ok){let g=y.closedSessions??[],t=g.filter(u=>!u.ok).length,e=g.length-t,n=g.length===0?"":t===0?`
220
+ </article>`,a.showModal(),a.querySelectorAll(".oncall-row").forEach(o=>{let c=o.dataset.bot,p=o.querySelector("input[data-action=toggle]"),s=o.querySelector("input[data-input=workingDir]"),f=o.querySelector("button[data-action=save]"),h=o.querySelector("[data-status]");p.addEventListener("change",()=>{s.disabled=!p.checked,p.checked&&s.focus()}),f.addEventListener("click",async()=>{h.textContent="",h.className="oncall-status";let t=p.checked,e=s.value.trim();if(t&&!e){h.textContent="\u8BF7\u586B\u5DE5\u4F5C\u76EE\u5F55",h.classList.add("hint-warn-inline");return}f.disabled=!0;try{let n=`/api/groups/${encodeURIComponent(i.chatId)}/oncall/${encodeURIComponent(c)}`,u=t?await fetch(n,{method:"PUT",headers:{"content-type":"application/json"},body:JSON.stringify({workingDir:e})}):await fetch(n,{method:"DELETE"}),d=await u.json().catch(()=>({}));if(u.ok&&d.ok){h.textContent=t?`\u2713 \u5DF2\u7ED1\u5B9A \u2192 ${d.resolvedPath??e}`:"\u2713 \u5DF2\u89E3\u7ED1",h.classList.add("hint-ok");try{await N(),g()}catch{}}else h.textContent=`\u2717 ${d.error??u.status}`,h.classList.add("hint-warn-inline")}catch(n){h.textContent=`\u2717 ${n?.message??n}`,h.classList.add("hint-warn-inline")}finally{f.disabled=!1}})}),a.querySelector("#g-leave-btn").onclick=async()=>{let o=[...a.querySelectorAll("input[name=leave-bot]:checked")].map(c=>c.value);if(o.length===0){alert("\u81F3\u5C11\u9009\u4E00\u4E2A\u673A\u5668\u4EBA");return}if(confirm(`\u786E\u5B9A\u8BA9 ${o.length} \u4E2A\u673A\u5668\u4EBA\u9000\u51FA\u7FA4\u804A\uFF1F\u8BE5 bot \u5728\u6B64\u7FA4\u7684\u4F1A\u8BDD\u4F1A\u4E00\u5E76\u5173\u95ED\u3002`))try{let p=await(await fetch(`/api/groups/${encodeURIComponent(i.chatId)}/leave`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({larkAppIds:o})})).json(),s=(p.result??[]).map(f=>{if(!f.ok)return`${f.larkAppId}: \u5931\u8D25 (${f.error??"unknown"})`;let h=f.closedSessions??[],t=h.filter(u=>!u.ok).length,e=h.length-t,n=h.length===0?"":t===0?`\uFF08\u5173\u95ED ${e} \u4E2A\u4F1A\u8BDD\uFF09`:`\uFF08\u5173\u95ED ${e} \u4E2A\uFF0C${t} \u4E2A\u5931\u8D25\uFF09`;return`${f.larkAppId}: OK${n}`}).join(`
221
+ `);alert(s||`Unexpected: ${JSON.stringify(p)}`),await N(),g()}catch(c){alert("Network error: "+c)}finally{a.close()}},a.querySelector("#g-disband-btn").onclick=async()=>{if(y.length===0||!confirm(`\u786E\u5B9A\u89E3\u6563\u7FA4\u804A\u300C${i.name??i.chatId}\u300D\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\uFF0C\u672C\u7FA4\u6240\u6709\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E5F\u4F1A\u4E00\u5E76\u5173\u95ED\u3002`))return;let o=[...y].sort((p,s)=>(s.larkAppId===S?1:0)-(p.larkAppId===S?1:0)),c=[];for(let p of o)try{let s=await fetch(`/api/groups/${encodeURIComponent(i.chatId)}/disband`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({larkAppId:p.larkAppId})}),f=await s.json();if(f.ok){let h=f.closedSessions??[],t=h.filter(u=>!u.ok).length,e=h.length-t,n=h.length===0?"":t===0?`
222
222
  \u5173\u95ED\u4E86 ${e} \u4E2A\u4F1A\u8BDD\u3002`:`
223
- \u5173\u95ED\u4E86 ${e} \u4E2A\u4F1A\u8BDD\uFF0C${t} \u4E2A\u4F1A\u8BDD\u5173\u95ED\u5931\u8D25\u3002`;alert(`\u5DF2\u89E3\u6563\uFF08\u7531 ${p.botName??p.larkAppId} \u6267\u884C\uFF09${n}`),await B(),m(),o.close();return}d.push(`${p.botName??p.larkAppId}: ${y.error??a.status}`)}catch(a){d.push(`${p.botName??p.larkAppId}: ${a}`)}alert(`\u6240\u6709\u5728\u7FA4\u673A\u5668\u4EBA\u5747\u65E0\u6CD5\u89E3\u6563\uFF1A
224
- ${d.join(`
223
+ \u5173\u95ED\u4E86 ${e} \u4E2A\u4F1A\u8BDD\uFF0C${t} \u4E2A\u4F1A\u8BDD\u5173\u95ED\u5931\u8D25\u3002`;alert(`\u5DF2\u89E3\u6563\uFF08\u7531 ${p.botName??p.larkAppId} \u6267\u884C\uFF09${n}`),await N(),g(),a.close();return}c.push(`${p.botName??p.larkAppId}: ${f.error??s.status}`)}catch(s){c.push(`${p.botName??p.larkAppId}: ${s}`)}alert(`\u6240\u6709\u5728\u7FA4\u673A\u5668\u4EBA\u5747\u65E0\u6CD5\u89E3\u6563\uFF1A
224
+ ${c.join(`
225
225
  `)}
226
226
 
227
- \u5EFA\u8BAE\u6539\u7528\u300C\u9000\u51FA\u7FA4\u804A\u300D\u3002`)}}w.addEventListener("input",m)}var O=document.getElementById("root");function K(){let r=location.hash||"#/";r.startsWith("#/groups")?z(O):r.startsWith("#/schedules")?G(O):U(O);for(let l of document.querySelectorAll("header nav a"))l.classList.toggle("active",l.getAttribute("href")===(r||"#/")||r==="#/"&&l.dataset.route==="sessions")}var P=document.getElementById("status");function V(){P&&(P.textContent=T.online?"\u25CF live":"\u25CF disconnected",P.className="status "+(T.online?"online":"offline"))}T.on(V);V();(async()=>{try{await _()}catch(r){console.error("botmux dashboard bootstrap failed",r),T.setOnline(!1)}window.addEventListener("hashchange",K),K()})();})();
227
+ \u5EFA\u8BAE\u6539\u7528\u300C\u9000\u51FA\u7FA4\u804A\u300D\u3002`)}}w.addEventListener("input",g)}var R=document.getElementById("root");function K(){let r=location.hash||"#/";r.startsWith("#/groups")?z(R):r.startsWith("#/schedules")?G(R):U(R);for(let l of document.querySelectorAll("header nav a"))l.classList.toggle("active",l.getAttribute("href")===(r||"#/")||r==="#/"&&l.dataset.route==="sessions")}var D=document.getElementById("status");function V(){D&&(D.textContent=T.online?"\u25CF live":"\u25CF disconnected",D.className="status "+(T.online?"online":"offline"))}T.on(V);V();(async()=>{try{await _()}catch(r){console.error("botmux dashboard bootstrap failed",r),T.setOnline(!1)}window.addEventListener("hashchange",K),K()})();})();
package/dist/worker.js CHANGED
@@ -1606,10 +1606,12 @@ let larkAppSecretForUpload = '';
1606
1606
  function startScreenshotLoop() {
1607
1607
  stopScreenshotLoop();
1608
1608
  screenshotTimer = setInterval(() => { void captureAndUpload(); }, SCREENSHOT_INTERVAL_MS);
1609
+ log(`Screenshot loop started (interval=${SCREENSHOT_INTERVAL_MS}ms)`);
1609
1610
  // Capture immediately so the user gets a first frame fast
1610
1611
  void captureAndUpload();
1611
1612
  }
1612
1613
  function stopScreenshotLoop() {
1614
+ const wasRunning = !!screenshotTimer || !!pendingShotTimer;
1613
1615
  if (screenshotTimer) {
1614
1616
  clearInterval(screenshotTimer);
1615
1617
  screenshotTimer = null;
@@ -1618,6 +1620,26 @@ function stopScreenshotLoop() {
1618
1620
  clearTimeout(pendingShotTimer);
1619
1621
  pendingShotTimer = null;
1620
1622
  }
1623
+ if (wasRunning)
1624
+ log('Screenshot loop stopped');
1625
+ }
1626
+ // Throttle silent-skip reasons so a wedged worker prints why once every 30s
1627
+ // without spamming. Each distinct reason has its own throttle clock.
1628
+ const screenshotSkipLogState = {};
1629
+ function logScreenshotSkip(reason) {
1630
+ const now = Date.now();
1631
+ if (now - (screenshotSkipLogState[reason] ?? 0) < 30_000)
1632
+ return;
1633
+ screenshotSkipLogState[reason] = now;
1634
+ log(`Screenshot skipped: ${reason}`);
1635
+ }
1636
+ // Worker stderr is piped through worker-pool, where most CLI stderr stays at
1637
+ // info level to avoid polluting error.log. Mark true worker faults so the
1638
+ // parent can selectively promote only these lines to logger.error.
1639
+ const WORKER_ERROR_MARKER = '[botmux-worker-error]';
1640
+ function logError(msg) {
1641
+ const ts = new Date().toISOString();
1642
+ process.stderr.write(`[${ts}] [worker:${sessionId.substring(0, 8) || '??'}] ${WORKER_ERROR_MARKER} ${msg}\n`);
1621
1643
  }
1622
1644
  /** Schedule a single capture +1s, then resume the regular 10s cadence. */
1623
1645
  function scheduleOneShotAfterAction() {
@@ -1638,17 +1660,29 @@ function scheduleOneShotAfterAction() {
1638
1660
  }, POST_ACTION_DELAY_MS);
1639
1661
  }
1640
1662
  async function captureAndUpload() {
1641
- if (displayMode !== 'screenshot')
1663
+ // displayMode mismatch should be impossible during a running loop (start/stop
1664
+ // gate on it). Logging here exists to surface the unexpected case — e.g. a
1665
+ // stray scheduleOneShotAfterAction firing after user toggled back to hidden.
1666
+ if (displayMode !== 'screenshot') {
1667
+ logScreenshotSkip(`displayMode=${displayMode}`);
1642
1668
  return;
1643
- if (awaitingFirstPrompt)
1669
+ }
1670
+ if (awaitingFirstPrompt) {
1671
+ logScreenshotSkip('awaitingFirstPrompt');
1644
1672
  return;
1645
- if (!renderer)
1673
+ }
1674
+ if (!renderer) {
1675
+ logScreenshotSkip('renderer=null');
1646
1676
  return;
1647
- if (!larkAppIdForUpload || !larkAppSecretForUpload)
1677
+ }
1678
+ if (!larkAppIdForUpload || !larkAppSecretForUpload) {
1679
+ logScreenshotSkip('lark credentials missing');
1648
1680
  return;
1681
+ }
1649
1682
  const term = renderer.xterm;
1650
1683
  const startY = term.buffer.active.baseY;
1651
- // Hash dedup — same content → skip upload
1684
+ // Hash dedup — same content → skip upload. Not logged: this is the expected
1685
+ // "nothing changed" path and would dominate the log signal.
1652
1686
  const snap = renderer.rawSnapshot();
1653
1687
  const hash = createHash('md5').update(snap).digest('hex');
1654
1688
  if (hash === lastShotHash)
@@ -1661,7 +1695,7 @@ async function captureAndUpload() {
1661
1695
  png = captureToPng(term, { cols: shotCols, rows: shotRows, startY });
1662
1696
  }
1663
1697
  catch (err) {
1664
- log(`Screenshot render failed: ${err.message}`);
1698
+ logError(`Screenshot render failed: ${err?.message ?? err}`);
1665
1699
  return;
1666
1700
  }
1667
1701
  let imageKey;
@@ -1669,7 +1703,7 @@ async function captureAndUpload() {
1669
1703
  imageKey = await uploadImageBuffer(larkAppIdForUpload, larkAppSecretForUpload, png);
1670
1704
  }
1671
1705
  catch (err) {
1672
- log(`Screenshot upload failed: ${err.message}`);
1706
+ logError(`Screenshot upload failed: ${err?.message ?? err}`);
1673
1707
  return;
1674
1708
  }
1675
1709
  let status = isPromptReady ? 'idle' : 'working';