botmux 2.12.2 → 2.13.0
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/README.en.md +29 -0
- package/README.md +29 -0
- package/dist/cli.js +104 -8
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/core/dashboard-events.d.ts +57 -0
- package/dist/core/dashboard-events.d.ts.map +1 -0
- package/dist/core/dashboard-events.js +23 -0
- package/dist/core/dashboard-events.js.map +1 -0
- package/dist/core/dashboard-ipc-server.d.ts +43 -0
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -0
- package/dist/core/dashboard-ipc-server.js +232 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -0
- package/dist/core/dashboard-locate.d.ts +20 -0
- package/dist/core/dashboard-locate.d.ts.map +1 -0
- package/dist/core/dashboard-locate.js +26 -0
- package/dist/core/dashboard-locate.js.map +1 -0
- package/dist/core/dashboard-rows.d.ts +30 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -0
- package/dist/core/dashboard-rows.js +48 -0
- package/dist/core/dashboard-rows.js.map +1 -0
- package/dist/core/scheduler.d.ts +20 -0
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +89 -2
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +19 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +146 -0
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +87 -4
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/aggregator.d.ts +41 -0
- package/dist/dashboard/aggregator.d.ts.map +1 -0
- package/dist/dashboard/aggregator.js +125 -0
- package/dist/dashboard/aggregator.js.map +1 -0
- package/dist/dashboard/auth.d.ts +23 -0
- package/dist/dashboard/auth.d.ts.map +1 -0
- package/dist/dashboard/auth.js +66 -0
- package/dist/dashboard/auth.js.map +1 -0
- package/dist/dashboard/registry.d.ts +28 -0
- package/dist/dashboard/registry.d.ts.map +1 -0
- package/dist/dashboard/registry.js +74 -0
- package/dist/dashboard/registry.js.map +1 -0
- package/dist/dashboard/web/app.d.ts +2 -0
- package/dist/dashboard/web/app.d.ts.map +1 -0
- package/dist/dashboard/web/app.js +42 -0
- package/dist/dashboard/web/app.js.map +1 -0
- package/dist/dashboard/web/groups.d.ts +2 -0
- package/dist/dashboard/web/groups.d.ts.map +1 -0
- package/dist/dashboard/web/groups.js +152 -0
- package/dist/dashboard/web/groups.js.map +1 -0
- package/dist/dashboard/web/schedules.d.ts +2 -0
- package/dist/dashboard/web/schedules.d.ts.map +1 -0
- package/dist/dashboard/web/schedules.js +105 -0
- package/dist/dashboard/web/schedules.js.map +1 -0
- package/dist/dashboard/web/sessions.d.ts +2 -0
- package/dist/dashboard/web/sessions.d.ts.map +1 -0
- package/dist/dashboard/web/sessions.js +184 -0
- package/dist/dashboard/web/sessions.js.map +1 -0
- package/dist/dashboard/web/store.d.ts +23 -0
- package/dist/dashboard/web/store.d.ts.map +1 -0
- package/dist/dashboard/web/store.js +82 -0
- package/dist/dashboard/web/store.js.map +1 -0
- package/dist/dashboard-web/app.js +129 -0
- package/dist/dashboard-web/index.html +22 -0
- package/dist/dashboard-web/style.css +50 -0
- package/dist/dashboard.d.ts +2 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +308 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/services/bridge-turn-queue.d.ts.map +1 -1
- package/dist/services/bridge-turn-queue.js +12 -7
- package/dist/services/bridge-turn-queue.js.map +1 -1
- package/dist/services/groups-store.d.ts +35 -0
- package/dist/services/groups-store.d.ts.map +1 -0
- package/dist/services/groups-store.js +104 -0
- package/dist/services/groups-store.js.map +1 -0
- package/dist/services/schedule-store.d.ts +1 -0
- package/dist/services/schedule-store.d.ts.map +1 -1
- package/dist/services/schedule-store.js +70 -1
- package/dist/services/schedule-store.js.map +1 -1
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +1 -0
- package/dist/services/session-store.js.map +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +34 -0
- package/dist/skills/definitions.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.js +47 -2
- package/dist/worker.js.map +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type Session = Record<string, any> & {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
status: string;
|
|
4
|
+
};
|
|
5
|
+
type Schedule = Record<string, any> & {
|
|
6
|
+
id: string;
|
|
7
|
+
};
|
|
8
|
+
declare class Store {
|
|
9
|
+
sessions: Map<string, Session>;
|
|
10
|
+
schedules: Map<string, Schedule>;
|
|
11
|
+
online: boolean;
|
|
12
|
+
private listeners;
|
|
13
|
+
upsertSessions(rows: Session[]): void;
|
|
14
|
+
upsertSchedules(rows: Schedule[]): void;
|
|
15
|
+
applySse(type: string, body: any): void;
|
|
16
|
+
setOnline(v: boolean): void;
|
|
17
|
+
on(fn: () => void): () => boolean;
|
|
18
|
+
private emit;
|
|
19
|
+
}
|
|
20
|
+
export declare const store: Store;
|
|
21
|
+
export declare function bootstrap(): Promise<void>;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/dashboard/web/store.ts"],"names":[],"mappings":"AACA,KAAK,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAC3E,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD,cAAM,KAAK;IACT,QAAQ,uBAA8B;IACtC,SAAS,wBAA+B;IACxC,MAAM,UAAQ;IACd,OAAO,CAAC,SAAS,CAAyB;IAE1C,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE;IAI9B,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE;IAIhC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;IAqBhC,SAAS,CAAC,CAAC,EAAE,OAAO;IAGpB,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI;IACjB,OAAO,CAAC,IAAI;CACb;AAED,eAAO,MAAM,KAAK,OAAc,CAAC;AAEjC,wBAAsB,SAAS,kBAwB9B"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
class Store {
|
|
2
|
+
sessions = new Map();
|
|
3
|
+
schedules = new Map();
|
|
4
|
+
online = true;
|
|
5
|
+
listeners = new Set();
|
|
6
|
+
upsertSessions(rows) {
|
|
7
|
+
for (const r of rows)
|
|
8
|
+
this.sessions.set(r.sessionId, r);
|
|
9
|
+
this.emit();
|
|
10
|
+
}
|
|
11
|
+
upsertSchedules(rows) {
|
|
12
|
+
for (const r of rows)
|
|
13
|
+
this.schedules.set(r.id, r);
|
|
14
|
+
this.emit();
|
|
15
|
+
}
|
|
16
|
+
applySse(type, body) {
|
|
17
|
+
if (type === 'session.spawned') {
|
|
18
|
+
this.sessions.set(body.session.sessionId, body.session);
|
|
19
|
+
}
|
|
20
|
+
else if (type === 'session.update') {
|
|
21
|
+
const cur = this.sessions.get(body.sessionId);
|
|
22
|
+
if (cur)
|
|
23
|
+
this.sessions.set(body.sessionId, { ...cur, ...body.patch });
|
|
24
|
+
}
|
|
25
|
+
else if (type === 'session.exited') {
|
|
26
|
+
const cur = this.sessions.get(body.sessionId);
|
|
27
|
+
if (cur)
|
|
28
|
+
this.sessions.set(body.sessionId, { ...cur, status: 'closed' });
|
|
29
|
+
}
|
|
30
|
+
else if (type === 'schedule.created') {
|
|
31
|
+
this.schedules.set(body.schedule.id, body.schedule);
|
|
32
|
+
}
|
|
33
|
+
else if (type === 'schedule.updated') {
|
|
34
|
+
const cur = this.schedules.get(body.id);
|
|
35
|
+
if (cur)
|
|
36
|
+
this.schedules.set(body.id, { ...cur, ...body.patch });
|
|
37
|
+
}
|
|
38
|
+
else if (type === 'schedule.deleted') {
|
|
39
|
+
this.schedules.delete(body.id);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
return; // heartbeat / schedule.fired — no cache mutation
|
|
43
|
+
}
|
|
44
|
+
this.emit();
|
|
45
|
+
}
|
|
46
|
+
setOnline(v) {
|
|
47
|
+
if (this.online !== v) {
|
|
48
|
+
this.online = v;
|
|
49
|
+
this.emit();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
on(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); }
|
|
53
|
+
emit() { for (const fn of this.listeners)
|
|
54
|
+
fn(); }
|
|
55
|
+
}
|
|
56
|
+
export const store = new Store();
|
|
57
|
+
export async function bootstrap() {
|
|
58
|
+
const [s, sch] = await Promise.all([
|
|
59
|
+
fetch('/api/sessions').then(r => r.json()),
|
|
60
|
+
fetch('/api/schedules').then(r => r.json()),
|
|
61
|
+
]);
|
|
62
|
+
store.upsertSessions(s.sessions ?? []);
|
|
63
|
+
store.upsertSchedules(sch.schedules ?? []);
|
|
64
|
+
const es = new EventSource('/events');
|
|
65
|
+
const types = [
|
|
66
|
+
'session.spawned', 'session.update', 'session.exited',
|
|
67
|
+
'schedule.created', 'schedule.updated', 'schedule.deleted',
|
|
68
|
+
'schedule.fired', 'heartbeat',
|
|
69
|
+
];
|
|
70
|
+
for (const t of types) {
|
|
71
|
+
es.addEventListener(t, e => {
|
|
72
|
+
try {
|
|
73
|
+
const data = JSON.parse(e.data);
|
|
74
|
+
store.applySse(t, data.body ?? data);
|
|
75
|
+
}
|
|
76
|
+
catch { /* skip malformed */ }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
es.onerror = () => store.setOnline(false);
|
|
80
|
+
es.onopen = () => store.setOnline(true);
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/dashboard/web/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";(()=>{var E=class{sessions=new Map;schedules=new Map;online=!0;listeners=new Set;upsertSessions(e){for(let a of e)this.sessions.set(a.sessionId,a);this.emit()}upsertSchedules(e){for(let a of e)this.schedules.set(a.id,a);this.emit()}applySse(e,a){if(e==="session.spawned")this.sessions.set(a.session.sessionId,a.session);else if(e==="session.update"){let c=this.sessions.get(a.sessionId);c&&this.sessions.set(a.sessionId,{...c,...a.patch})}else if(e==="session.exited"){let c=this.sessions.get(a.sessionId);c&&this.sessions.set(a.sessionId,{...c,status:"closed"})}else if(e==="schedule.created")this.schedules.set(a.schedule.id,a.schedule);else if(e==="schedule.updated"){let c=this.schedules.get(a.id);c&&this.schedules.set(a.id,{...c,...a.patch})}else if(e==="schedule.deleted")this.schedules.delete(a.id);else return;this.emit()}setOnline(e){this.online!==e&&(this.online=e,this.emit())}on(e){return this.listeners.add(e),()=>this.listeners.delete(e)}emit(){for(let e of this.listeners)e()}},h=new E;async function x(){let[n,e]=await Promise.all([fetch("/api/sessions").then(f=>f.json()),fetch("/api/schedules").then(f=>f.json())]);h.upsertSessions(n.sessions??[]),h.upsertSchedules(e.schedules??[]);let a=new EventSource("/events"),c=["session.spawned","session.update","session.exited","schedule.created","schedule.updated","schedule.deleted","schedule.fired","heartbeat"];for(let f of c)a.addEventListener(f,i=>{try{let p=JSON.parse(i.data);h.applySse(f,p.body??p)}catch{}});a.onerror=()=>h.setOnline(!1),a.onopen=()=>h.setOnline(!0)}var O=`
|
|
2
|
+
<form id="filters" class="filters">
|
|
3
|
+
<input type="search" name="q" placeholder="search workingDir / title / ids" />
|
|
4
|
+
<select name="cli" multiple size="4">
|
|
5
|
+
<option value="claude-code">claude-code</option>
|
|
6
|
+
<option value="codex">codex</option>
|
|
7
|
+
<option value="gemini">gemini</option>
|
|
8
|
+
<option value="opencode">opencode</option>
|
|
9
|
+
<option value="aiden">aiden</option>
|
|
10
|
+
<option value="coco">coco</option>
|
|
11
|
+
<option value="unknown">unknown</option>
|
|
12
|
+
</select>
|
|
13
|
+
<select name="status">
|
|
14
|
+
<option value="">any status</option>
|
|
15
|
+
<option>starting</option><option>working</option>
|
|
16
|
+
<option>idle</option><option>analyzing</option><option>closed</option>
|
|
17
|
+
</select>
|
|
18
|
+
<select name="adopt">
|
|
19
|
+
<option value="">adopt: any</option>
|
|
20
|
+
<option value="yes">adopt: yes</option>
|
|
21
|
+
<option value="no">adopt: no</option>
|
|
22
|
+
</select>
|
|
23
|
+
<label><input type="checkbox" name="active" checked> active only</label>
|
|
24
|
+
</form>
|
|
25
|
+
<table id="sessions-table">
|
|
26
|
+
<thead><tr>
|
|
27
|
+
<th>bot</th><th>cli</th><th>status</th><th>title</th><th>workingDir</th>
|
|
28
|
+
<th>spawned</th><th>last</th><th>adopt</th><th></th>
|
|
29
|
+
</tr></thead>
|
|
30
|
+
<tbody></tbody>
|
|
31
|
+
</table>
|
|
32
|
+
<dialog id="drawer"></dialog>
|
|
33
|
+
`;function C(n){if(!n)return"-";let e=Date.now()-n;return e<6e4?"now":e<36e5?Math.floor(e/6e4)+"m":e<864e5?Math.floor(e/36e5)+"h":Math.floor(e/864e5)+"d"}function u(n){return n.replace(/[&<>"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}var B="\u{1FA9E}",y="\u{1F4CD}",F="\u{1F5A5}\uFE0F";function A(n){n.innerHTML=O;let e=n.querySelector("#sessions-table tbody"),a=n.querySelector("#filters"),c=n.querySelector("#drawer");function f(t){return`<tr data-id="${u(t.sessionId)}">
|
|
34
|
+
<td>${u(t.botName??"")}</td>
|
|
35
|
+
<td><span class="badge cli-${u(t.cliId??"unknown")}">${u(t.cliId??"unknown")}</span></td>
|
|
36
|
+
<td><span class="status status-${u(t.status)}">${u(t.status)}</span></td>
|
|
37
|
+
<td>${u((t.title??"").slice(0,40))}</td>
|
|
38
|
+
<td title="${u(t.workingDir??"")}">${u((t.workingDir??"").slice(-30))}</td>
|
|
39
|
+
<td>${C(t.spawnedAt)}</td>
|
|
40
|
+
<td>${C(t.lastMessageAt)}</td>
|
|
41
|
+
<td>${t.adopt?B:""}</td>
|
|
42
|
+
<td><button class="open">\u22EF</button></td>
|
|
43
|
+
</tr>`}function i(){let t=new FormData(a),r=(t.get("q")??"").toLowerCase(),l=t.getAll("cli"),d=t.get("status"),s=t.get("adopt"),b=!!t.get("active");return[...h.sessions.values()].filter(o=>!l.length||l.includes(o.cliId??"unknown")).filter(o=>!d||o.status===d).filter(o=>!s||s==="yes"==!!o.adopt).filter(o=>!b||o.status!=="closed").filter(o=>!r||JSON.stringify(o).toLowerCase().includes(r)).sort((o,w)=>(w.lastMessageAt??0)-(o.lastMessageAt??0))}function p(){e.innerHTML=i().map(f).join("")}function m(t){let r=t.status==="closed";c.innerHTML=`
|
|
44
|
+
<article>
|
|
45
|
+
<header>
|
|
46
|
+
<h3>${u(t.title??t.sessionId)}</h3>
|
|
47
|
+
<code>${u(t.sessionId)}</code> <button data-copy="${u(t.sessionId)}">copy</button>
|
|
48
|
+
</header>
|
|
49
|
+
<p><b>bot:</b> ${u(t.botName??"-")} \xB7 <b>cli:</b> ${u(t.cliId??"?")} \xB7 <b>status:</b> ${u(t.status)}</p>
|
|
50
|
+
<p><b>chatId:</b> <code>${u(t.chatId)}</code> <button data-copy="${u(t.chatId)}">copy</button></p>
|
|
51
|
+
<p><b>rootMessageId:</b> <code>${u(t.rootMessageId??"")}</code> <button data-copy="${u(t.rootMessageId??"")}">copy</button></p>
|
|
52
|
+
${t.threadId?`<p><b>threadId:</b> <code>${u(t.threadId)}</code></p>`:""}
|
|
53
|
+
<p><b>workingDir:</b> ${u(t.workingDir??"-")}</p>
|
|
54
|
+
<div class="actions">
|
|
55
|
+
<button id="locate-btn" type="button">${y} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898</button>
|
|
56
|
+
${t.webPort?`<a class="btn-link" href="http://${u(location.hostname)}:${t.webPort}" target="_blank">${F} \u6253\u5F00 xterm</a>`:""}
|
|
57
|
+
${r?"":'<button id="close-btn" type="button" class="contrast">\u5173\u95ED\u4F1A\u8BDD</button>'}
|
|
58
|
+
</div>
|
|
59
|
+
<form method="dialog"><button>\u5173\u95ED</button></form>
|
|
60
|
+
</article>`,c.querySelectorAll("[data-copy]").forEach(s=>{s.onclick=()=>{navigator.clipboard.writeText(s.dataset.copy??""),s.textContent="copied",setTimeout(()=>{s.textContent="copy"},800)}});let l=c.querySelector("#locate-btn");l&&(l.onclick=async()=>{l.disabled=!0,l.textContent=`${y} \u53D1\u9001\u4E2D...`;try{let s=await fetch(`/api/sessions/${encodeURIComponent(t.sessionId)}/locate`,{method:"POST"}),b=await s.json();if(b.ok){window.open(t.feishuChatLink,"_blank");let o=30;l.textContent=`${y} (\u51B7\u5374 ${o}s)`;let w=setInterval(()=>{o-=1,o<=0?(clearInterval(w),l.disabled=!1,l.textContent=`${y} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898`):l.textContent=`${y} (\u51B7\u5374 ${o}s)`},1e3)}else{let o=b.error??s.status;alert("Locate failed: "+o),l.disabled=!1,l.textContent=`${y} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898`}}catch(s){alert("Locate error: "+s),l.disabled=!1,l.textContent=`${y} \u5B9A\u4F4D\u5230\u98DE\u4E66\u8BDD\u9898`}});let d=c.querySelector("#close-btn");d&&(d.onclick=async()=>{if(confirm("\u5173\u95ED\u8FD9\u4E2A\u4F1A\u8BDD?")){d.disabled=!0;try{await fetch(`/api/sessions/${encodeURIComponent(t.sessionId)}/close`,{method:"POST"})}finally{c.close()}}}),c.showModal()}e.addEventListener("click",t=>{let r=t.target.closest("tr[data-id]");if(!r)return;let l=r.dataset.id,d=h.sessions.get(l);d&&m(d)}),a.addEventListener("input",p),h.on(p),p()}var j=`
|
|
61
|
+
<form id="sched-filters" class="filters">
|
|
62
|
+
<input type="search" name="q" placeholder="search name / prompt / workingDir" />
|
|
63
|
+
<select name="kind">
|
|
64
|
+
<option value="">any kind</option>
|
|
65
|
+
<option>cron</option>
|
|
66
|
+
<option>interval</option>
|
|
67
|
+
<option>once</option>
|
|
68
|
+
</select>
|
|
69
|
+
<label><input type="checkbox" name="enabled"> enabled only</label>
|
|
70
|
+
</form>
|
|
71
|
+
<table>
|
|
72
|
+
<thead><tr>
|
|
73
|
+
<th>name</th><th>bot</th><th>schedule</th><th>next</th><th>last</th>
|
|
74
|
+
<th>repeat</th><th>enabled</th><th>actions</th>
|
|
75
|
+
</tr></thead>
|
|
76
|
+
<tbody id="schedules-tbody"></tbody>
|
|
77
|
+
</table>
|
|
78
|
+
`;function M(n){return n.replace(/[&<>"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}function q(n){if(!n)return"\u2014";try{return new Date(n).toLocaleString()}catch{return n}}function N(n){n.innerHTML=j;let e=n.querySelector("#schedules-tbody"),a=n.querySelector("#sched-filters");function c(){let i=new FormData(a),p=(i.get("q")??"").toLowerCase(),m=i.get("kind"),t=!!i.get("enabled");return[...h.schedules.values()].filter(r=>!m||r.parsed?.kind===m).filter(r=>!t||r.enabled).filter(r=>!p||JSON.stringify(r).toLowerCase().includes(p)).sort((r,l)=>{if(r.enabled!==l.enabled)return r.enabled?-1:1;let d=r.nextRunAt?Date.parse(r.nextRunAt):1/0,s=l.nextRunAt?Date.parse(l.nextRunAt):1/0;return d-s})}function f(){e.innerHTML=c().map(i=>`<tr data-id="${M(i.id)}">
|
|
79
|
+
<td>${M(i.name??i.id)}</td>
|
|
80
|
+
<td>${M(i.botName??i.larkAppId??"-")}</td>
|
|
81
|
+
<td><code>${M(i.parsed?.display??"?")}</code></td>
|
|
82
|
+
<td>${q(i.nextRunAt)}</td>
|
|
83
|
+
<td>${q(i.lastRunAt)} ${i.lastStatus==="error"?"\u26A0\uFE0F":""}</td>
|
|
84
|
+
<td>${i.repeat?`${i.repeat.completed}/${i.repeat.times??"\u221E"}`:"\u2014"}</td>
|
|
85
|
+
<td>${i.enabled?"\u2713":"\u2717"}</td>
|
|
86
|
+
<td class="actions-cell">
|
|
87
|
+
<button data-op="run" type="button">Run now</button>
|
|
88
|
+
${i.enabled?'<button data-op="pause" type="button">Pause</button>':'<button data-op="resume" type="button">Resume</button>'}
|
|
89
|
+
</td>
|
|
90
|
+
</tr>`).join("")||'<tr><td colspan="8" class="empty">No schedules.</td></tr>'}e.addEventListener("click",async i=>{let p=i.target.closest("button[data-op]");if(!p)return;let m=p.closest("tr[data-id]");if(!m)return;let t=m.dataset.id,r=p.dataset.op;p.disabled=!0;let l=p.textContent;p.textContent="...";try{let d=await fetch(`/api/schedules/${encodeURIComponent(t)}/${r}`,{method:"POST"}),s=await d.json().catch(()=>({}));(!d.ok||s.ok===!1)&&alert(`Failed: ${d.status} ${s?.error??""}`.trim())}catch(d){alert("Network error: "+d)}finally{p.disabled=!1,p.textContent=l}}),a.addEventListener("input",f),h.on(f),f()}var $={chats:[],bots:[]},G=`
|
|
91
|
+
<form id="g-filters" class="filters">
|
|
92
|
+
<input type="search" name="q" placeholder="search chat name / id / owner" />
|
|
93
|
+
<label><input type="checkbox" name="missing"> missing-bot only</label>
|
|
94
|
+
<button type="button" id="g-refresh">Refresh</button>
|
|
95
|
+
</form>
|
|
96
|
+
<table>
|
|
97
|
+
<thead id="g-head"></thead>
|
|
98
|
+
<tbody id="g-body"></tbody>
|
|
99
|
+
</table>
|
|
100
|
+
<dialog id="g-drawer"></dialog>
|
|
101
|
+
`;function g(n){return n.replace(/[&<>"']/g,e=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[e])}async function S(){$=await(await fetch("/api/groups")).json()}async function R(n){n.innerHTML=G;let e=n.querySelector("#g-head"),a=n.querySelector("#g-body"),c=n.querySelector("#g-filters"),f=n.querySelector("#g-refresh"),i=n.querySelector("#g-drawer");f.onclick=async()=>{f.disabled=!0;try{await S(),m()}finally{f.disabled=!1}},await S();function p(){e.innerHTML=`<tr>
|
|
102
|
+
<th>chat</th>
|
|
103
|
+
${$.bots.map(t=>`<th>${g(t.botName??t.larkAppId)}</th>`).join("")}
|
|
104
|
+
<th>actions</th>
|
|
105
|
+
</tr>`}function m(){p();let t=new FormData(c),r=(t.get("q")??"").toLowerCase(),l=!!t.get("missing"),d=$.chats.filter(s=>!r||(s.name??"").toLowerCase().includes(r)||s.chatId.toLowerCase().includes(r)||(s.ownerId??"").toLowerCase().includes(r)).filter(s=>!l||s.memberBots.some(b=>!b.inChat));if(d.length===0){a.innerHTML=`<tr><td colspan="${$.bots.length+2}" class="empty">No chats match the filter.</td></tr>`;return}a.innerHTML=d.map(s=>`<tr data-chat="${g(s.chatId)}">
|
|
106
|
+
<td>
|
|
107
|
+
<strong>${g(s.name??s.chatId)}</strong><br>
|
|
108
|
+
<small><code>${g(s.chatId)}</code></small>
|
|
109
|
+
</td>
|
|
110
|
+
${$.bots.map(b=>{let o=s.memberBots.find(L=>L.larkAppId===b.larkAppId),w=o?o.error?"!":o.inChat?"\u2713":"\u2717":"?";return`<td class="${o?o.error?"cell-error":o.inChat?"cell-in":"cell-out":"cell-unknown"}" title="${g(o?.error??"")}">${w}</td>`}).join("")}
|
|
111
|
+
<td><button class="add-bots" type="button">Add bots</button></td>
|
|
112
|
+
</tr>`).join("")}m(),a.addEventListener("click",async t=>{let r=t.target.closest("button.add-bots");if(!r)return;let d=r.closest("tr[data-chat]").dataset.chat,s=$.chats.find(o=>o.chatId===d);if(!s)return;let b=s.memberBots.filter(o=>!o.inChat);if(!b.length){alert("All configured bots are already in this chat.");return}i.innerHTML=`
|
|
113
|
+
<article>
|
|
114
|
+
<header><h3>Add bots to ${g(s.name??s.chatId)}</h3></header>
|
|
115
|
+
<p>Select bots to add. The dashboard will pick a bot that's already in the chat as the proxy.</p>
|
|
116
|
+
<form id="g-addform">
|
|
117
|
+
${b.map(o=>`
|
|
118
|
+
<label class="checkbox-row">
|
|
119
|
+
<input type="checkbox" name="bot" value="${g(o.larkAppId)}">
|
|
120
|
+
${g(o.botName??o.larkAppId)} <small>(${g(o.larkAppId)})</small>
|
|
121
|
+
</label>
|
|
122
|
+
`).join("")}
|
|
123
|
+
<div class="actions">
|
|
124
|
+
<button type="submit">Confirm add</button>
|
|
125
|
+
<button type="button" id="g-cancel">Cancel</button>
|
|
126
|
+
</div>
|
|
127
|
+
</form>
|
|
128
|
+
</article>`,i.showModal(),i.querySelector("#g-cancel").onclick=()=>i.close(),i.querySelector("#g-addform").onsubmit=async o=>{o.preventDefault();let k=new FormData(o.target).getAll("bot");if(k.length===0){alert("Pick at least one bot.");return}try{let I=await(await fetch(`/api/groups/${encodeURIComponent(d)}/add-bots`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({larkAppIds:k})})).json();if(I.error==="no_proxy_bot")alert("No bot is currently in this chat \u2014 add one manually in Feishu first, then retry.");else if(I.result){let _=I.result.map(T=>`${T.id}: ${T.ok?"OK":`failed (${T.error??"unknown"})`}`).join(`
|
|
129
|
+
`);alert(_),await S(),m()}else alert(`Unexpected response: ${JSON.stringify(I)}`)}catch(L){alert("Network error: "+L)}finally{i.close()}}}),c.addEventListener("input",m)}var v=document.getElementById("root");function D(){let n=location.hash||"#/";n.startsWith("#/groups")?R(v):n.startsWith("#/schedules")?N(v):A(v);for(let e of document.querySelectorAll("header nav a"))e.classList.toggle("active",e.getAttribute("href")===(n||"#/")||n==="#/"&&e.dataset.route==="sessions")}var H=document.getElementById("status");function P(){H&&(H.textContent=h.online?"\u25CF live":"\u25CF disconnected",H.className="status "+(h.online?"online":"offline"))}h.on(P);P();(async()=>{try{await x()}catch(n){console.error("botmux dashboard bootstrap failed",n),h.setOnline(!1)}window.addEventListener("hashchange",D),D()})();})();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>botmux dashboard</title>
|
|
7
|
+
<link rel="stylesheet" href="/assets/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header class="topbar">
|
|
11
|
+
<strong>botmux</strong>
|
|
12
|
+
<nav>
|
|
13
|
+
<a href="#/" data-route="sessions">Sessions</a>
|
|
14
|
+
<a href="#/schedules" data-route="schedules">Schedules</a>
|
|
15
|
+
<a href="#/groups" data-route="groups">Groups & Bots</a>
|
|
16
|
+
</nav>
|
|
17
|
+
<span id="status" class="status"></span>
|
|
18
|
+
</header>
|
|
19
|
+
<main id="root"></main>
|
|
20
|
+
<script src="/assets/app.js"></script>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--fg: #1f2328;
|
|
3
|
+
--muted: #57606a;
|
|
4
|
+
--border: #d0d7de;
|
|
5
|
+
--bg: #ffffff;
|
|
6
|
+
--header-bg: #f6f8fa;
|
|
7
|
+
}
|
|
8
|
+
* { box-sizing: border-box; }
|
|
9
|
+
body { margin: 0; font: 14px/1.4 system-ui, -apple-system, "Segoe UI", sans-serif; color: var(--fg); background: var(--bg); }
|
|
10
|
+
.topbar { display: flex; align-items: center; gap: 1.5rem; padding: 0.6rem 1rem; background: var(--header-bg); border-bottom: 1px solid var(--border); }
|
|
11
|
+
.topbar nav a { margin-right: 0.6rem; color: var(--muted); text-decoration: none; }
|
|
12
|
+
.topbar nav a.active { color: var(--fg); font-weight: 600; }
|
|
13
|
+
.status { margin-left: auto; font-size: 12px; color: var(--muted); }
|
|
14
|
+
.status.online { color: #1a7f37; }
|
|
15
|
+
.status.offline { color: #cf222e; }
|
|
16
|
+
main { padding: 1rem; }
|
|
17
|
+
.filters { display: flex; gap: 0.6rem; margin-bottom: 0.8rem; flex-wrap: wrap; align-items: center; }
|
|
18
|
+
.filters input[type=search] { min-width: 240px; padding: 0.3rem 0.5rem; border: 1px solid var(--border); border-radius: 4px; }
|
|
19
|
+
.filters select { padding: 0.2rem; border: 1px solid var(--border); border-radius: 4px; }
|
|
20
|
+
table { border-collapse: collapse; width: 100%; font-variant-numeric: tabular-nums; }
|
|
21
|
+
th, td { padding: 0.4rem 0.6rem; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
|
22
|
+
th { background: var(--header-bg); font-weight: 600; }
|
|
23
|
+
tr[data-id]:hover { background: #f6f8fa; cursor: pointer; }
|
|
24
|
+
button { padding: 0.25rem 0.6rem; border: 1px solid var(--border); background: white; border-radius: 4px; cursor: pointer; }
|
|
25
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
26
|
+
button.contrast { background: #cf222e; color: white; border-color: #cf222e; }
|
|
27
|
+
.btn-link { display: inline-block; padding: 0.25rem 0.6rem; border: 1px solid var(--border); border-radius: 4px; text-decoration: none; color: var(--fg); }
|
|
28
|
+
.badge { padding: 0.05rem 0.4rem; border-radius: 999px; font-size: 11px; font-variant: small-caps; }
|
|
29
|
+
.cli-claude-code { background: #d6e4ff; }
|
|
30
|
+
.cli-codex { background: #ffe5cc; }
|
|
31
|
+
.cli-gemini { background: #ffd6e7; }
|
|
32
|
+
.cli-opencode { background: #d4f4dd; }
|
|
33
|
+
.cli-aiden { background: #fff3a8; }
|
|
34
|
+
.cli-coco { background: #c8e6c9; }
|
|
35
|
+
.cli-unknown { background: #eee; color: var(--muted); }
|
|
36
|
+
.status-working { color: #0969da; font-weight: 600; }
|
|
37
|
+
.status-idle { color: var(--muted); }
|
|
38
|
+
.status-closed { color: #999; text-decoration: line-through; }
|
|
39
|
+
.status-analyzing { color: #bf8700; }
|
|
40
|
+
.status-starting { color: #8250df; }
|
|
41
|
+
dialog#drawer { border: none; border-radius: 8px; padding: 0; max-width: 600px; }
|
|
42
|
+
dialog#drawer article { padding: 1rem 1.5rem; }
|
|
43
|
+
dialog#drawer code { background: var(--header-bg); padding: 0.05rem 0.3rem; border-radius: 3px; font-size: 12px; }
|
|
44
|
+
.actions { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 0.8rem 0; }
|
|
45
|
+
.cell-in { color: #1a7f37; text-align: center; }
|
|
46
|
+
.cell-out { color: #999; text-align: center; }
|
|
47
|
+
.cell-error { color: #cf222e; text-align: center; }
|
|
48
|
+
.cell-unknown { color: #bf8700; text-align: center; }
|
|
49
|
+
.checkbox-row { display: block; padding: 0.3rem 0; }
|
|
50
|
+
.empty { color: var(--muted); padding: 1rem; text-align: center; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dashboard.d.ts","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":""}
|