@weiping/mbridge-page 0.2.0 → 0.3.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/package.json +2 -4
- package/page/app.js +32 -141
- package/page/index.html +1 -34
- package/page/view.js +3 -99
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weiping/mbridge-page",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "mbridge's usage at a glance inside magpie: what magpie's ledger recorded, and where mbridge's own status page is.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -33,9 +33,7 @@
|
|
|
33
33
|
"file": "page/index.html",
|
|
34
34
|
"icon": "page/icon.svg"
|
|
35
35
|
},
|
|
36
|
-
"permissions": [
|
|
37
|
-
"usage:summary"
|
|
38
|
-
],
|
|
36
|
+
"permissions": [],
|
|
39
37
|
"options": {
|
|
40
38
|
"period": "7d",
|
|
41
39
|
"sort": "tokens"
|
package/page/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// app.js reads
|
|
2
|
-
// the sandbox a magpie page runs in
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// app.js reads what mbridge published into the plugin's options, and
|
|
2
|
+
// draws it. It never reaches mbridge: the sandbox a magpie page runs in
|
|
3
|
+
// has no network at all (connect-src 'none'), so mbridge pushes its
|
|
4
|
+
// state into the options and the page reads them in-process (SP14 §7).
|
|
5
5
|
//
|
|
6
6
|
// A classic script, like the community's usage page: it reads the view
|
|
7
7
|
// from globalThis.
|
|
@@ -26,25 +26,21 @@
|
|
|
26
26
|
const theme = await magpie.ready;
|
|
27
27
|
let lang = theme.lang;
|
|
28
28
|
|
|
29
|
-
// the
|
|
30
|
-
//
|
|
29
|
+
// the page's own keys in the options (kept so mbridge's key survives
|
|
30
|
+
// a write)
|
|
31
31
|
let opts = {};
|
|
32
32
|
try {
|
|
33
33
|
opts = (await magpie.options.get()) || {};
|
|
34
34
|
} catch {
|
|
35
|
-
// no options yet:
|
|
35
|
+
// no options yet: mbridge publishes them
|
|
36
36
|
}
|
|
37
37
|
const state = {
|
|
38
|
-
period: V.PERIODS.includes(opts.period) ? opts.period : "7d",
|
|
39
|
-
sort: V.SORTS.includes(opts.sort) ? opts.sort : "tokens",
|
|
40
|
-
summary: null,
|
|
41
|
-
error: null,
|
|
42
|
-
// what mbridge published into the options (SP14 §7)
|
|
43
38
|
mbridge: null,
|
|
44
|
-
// the sequence number of the last command the page sent
|
|
45
39
|
cmdSeq: 0,
|
|
46
40
|
};
|
|
47
41
|
|
|
42
|
+
// the address of mbridge's own page: selectable text, not a link (the
|
|
43
|
+
// sandbox may navigate itself, but that is a one-way ticket, SP14 §2)
|
|
48
44
|
function words() {
|
|
49
45
|
const t = V.text(lang);
|
|
50
46
|
document.querySelectorAll("[data-t]").forEach((e) => {
|
|
@@ -53,16 +49,23 @@
|
|
|
53
49
|
$("status").textContent = t.status;
|
|
54
50
|
$("restart").textContent = t.restart;
|
|
55
51
|
$("cmd").textContent = "mbridge service restart";
|
|
52
|
+
$("addr").textContent = V.STATUS_URL;
|
|
53
|
+
$("mbridge-ask").textContent = t.mbridgeAsk;
|
|
54
|
+
$("mbridge-restart").textContent = t.mbridgeRestart;
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
// readOptions reads the options: what mbridge published. What we write
|
|
58
|
+
// back must be what we just read, so mbridge's snapshot is not put back
|
|
59
|
+
// stale.
|
|
60
|
+
async function readOptions() {
|
|
61
|
+
try {
|
|
62
|
+
opts = (await magpie.options.get()) || {};
|
|
63
|
+
state.mbridge = opts.mbridge || null;
|
|
64
|
+
} catch {
|
|
65
|
+
// the options are not readable: keep what we have
|
|
66
|
+
}
|
|
62
67
|
}
|
|
63
68
|
|
|
64
|
-
// paintMbridge draws what mbridge published: its numbers, the sessions
|
|
65
|
-
// running now, and the decisions it made.
|
|
66
69
|
function paintMbridge() {
|
|
67
70
|
const t = V.text(lang);
|
|
68
71
|
const v = V.mbridgeView(state.mbridge, lang);
|
|
@@ -134,85 +137,7 @@
|
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
function paint() {
|
|
137
|
-
pick("periods", "period", state.period);
|
|
138
|
-
pick("sorts", "sort", state.sort);
|
|
139
|
-
// mbridge's part does not depend on magpie's usage
|
|
140
140
|
paintMbridge();
|
|
141
|
-
const s = state.summary;
|
|
142
|
-
if (!s) return;
|
|
143
|
-
$("since").textContent = V.since(s, lang);
|
|
144
|
-
|
|
145
|
-
$("tiles").replaceChildren(
|
|
146
|
-
...V.tiles(s, lang).map((tile) => {
|
|
147
|
-
const cell = document.createElement("div");
|
|
148
|
-
cell.className = "tile";
|
|
149
|
-
for (const [cls, value] of [["k", tile.label], ["v", tile.value], ["s", tile.sub]]) {
|
|
150
|
-
if (value === undefined) continue;
|
|
151
|
-
const e = document.createElement("div");
|
|
152
|
-
e.className = cls;
|
|
153
|
-
e.textContent = value;
|
|
154
|
-
cell.append(e);
|
|
155
|
-
}
|
|
156
|
-
return cell;
|
|
157
|
-
})
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
const rows = V.rows(s, state.sort, lang);
|
|
161
|
-
$("rows").replaceChildren(
|
|
162
|
-
...rows.map((r) => {
|
|
163
|
-
const tr = document.createElement("tr");
|
|
164
|
-
const name = document.createElement("td");
|
|
165
|
-
name.className = "model";
|
|
166
|
-
name.textContent = r.model;
|
|
167
|
-
const bar = document.createElement("span");
|
|
168
|
-
bar.className = "bar";
|
|
169
|
-
const fill = document.createElement("i");
|
|
170
|
-
fill.style.width = Math.max(2, Math.round(r.shareValue * 100)) + "%";
|
|
171
|
-
bar.append(fill);
|
|
172
|
-
name.append(bar);
|
|
173
|
-
const calls = document.createElement("td");
|
|
174
|
-
calls.className = "num";
|
|
175
|
-
calls.textContent = r.calls;
|
|
176
|
-
const tokens = document.createElement("td");
|
|
177
|
-
tokens.className = "num";
|
|
178
|
-
tokens.textContent = r.tokens;
|
|
179
|
-
const cost = document.createElement("td");
|
|
180
|
-
cost.className = "num";
|
|
181
|
-
cost.textContent = r.cost;
|
|
182
|
-
tr.append(name, calls, tokens, cost);
|
|
183
|
-
return tr;
|
|
184
|
-
})
|
|
185
|
-
);
|
|
186
|
-
$("empty").hidden = rows.length > 0;
|
|
187
|
-
$("empty").textContent = V.text(lang).empty;
|
|
188
|
-
$("error").hidden = !state.error;
|
|
189
|
-
if (state.error) {
|
|
190
|
-
$("error").textContent = V.text(lang).failed + ": " + state.error + " · " + V.text(lang).retry;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// readOptions reads the plugin's options: the page's period and sort,
|
|
195
|
-
// and what mbridge published there.
|
|
196
|
-
async function readOptions() {
|
|
197
|
-
try {
|
|
198
|
-
const o = (await magpie.options.get()) || {};
|
|
199
|
-
if (V.PERIODS.includes(o.period)) state.period = o.period;
|
|
200
|
-
if (V.SORTS.includes(o.sort)) state.sort = o.sort;
|
|
201
|
-
state.mbridge = o.mbridge || null;
|
|
202
|
-
} catch {
|
|
203
|
-
// the options are not readable: keep what we have
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async function load() {
|
|
208
|
-
await readOptions();
|
|
209
|
-
try {
|
|
210
|
-
state.summary = await magpie.read("usage:summary", { period: state.period });
|
|
211
|
-
state.error = null;
|
|
212
|
-
} catch (e) {
|
|
213
|
-
state.error = (e && e.message) || String(e);
|
|
214
|
-
}
|
|
215
|
-
paint();
|
|
216
141
|
}
|
|
217
142
|
|
|
218
143
|
// command asks mbridge to do something. The sequence number makes it
|
|
@@ -225,54 +150,13 @@
|
|
|
225
150
|
const o = (await magpie.options.get()) || {};
|
|
226
151
|
o.cmd = cmd;
|
|
227
152
|
await magpie.options.set(o);
|
|
228
|
-
} catch
|
|
153
|
+
} catch {
|
|
229
154
|
// mbridge will not hear it; the page says so
|
|
230
155
|
return false;
|
|
231
156
|
}
|
|
232
157
|
return true;
|
|
233
158
|
}
|
|
234
159
|
|
|
235
|
-
// magpie takes one options write a second: a quick second click is
|
|
236
|
-
// saved with the next one
|
|
237
|
-
let saving = null;
|
|
238
|
-
function save() {
|
|
239
|
-
if (saving) return;
|
|
240
|
-
saving = setTimeout(async () => {
|
|
241
|
-
saving = null;
|
|
242
|
-
try {
|
|
243
|
-
// the whole object is replaced: mbridge's key goes with it, and
|
|
244
|
-
// mbridge puts its snapshot back on its next round
|
|
245
|
-
opts = (await magpie.options.set({ ...opts, period: state.period, sort: state.sort })) || opts;
|
|
246
|
-
} catch {
|
|
247
|
-
// not remembering is not worth an error on the page
|
|
248
|
-
}
|
|
249
|
-
}, 1100);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
$("periods").addEventListener("click", (e) => {
|
|
253
|
-
const b = e.target.closest("button[data-period]");
|
|
254
|
-
if (!b || b.dataset.period === state.period) return;
|
|
255
|
-
state.period = b.dataset.period;
|
|
256
|
-
save();
|
|
257
|
-
load();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
$("sorts").addEventListener("click", (e) => {
|
|
261
|
-
const b = e.target.closest("button[data-sort]");
|
|
262
|
-
if (!b || b.dataset.sort === state.sort) return;
|
|
263
|
-
state.sort = b.dataset.sort;
|
|
264
|
-
save();
|
|
265
|
-
paint();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// the period and sort buttons carry their own words
|
|
269
|
-
for (const b of $("periods").querySelectorAll("button[data-period]")) {
|
|
270
|
-
b.textContent = V.periodLabel(b.dataset.period, lang);
|
|
271
|
-
}
|
|
272
|
-
for (const b of $("sorts").querySelectorAll("button[data-sort]")) {
|
|
273
|
-
b.textContent = V.sortLabel(b.dataset.sort, lang);
|
|
274
|
-
}
|
|
275
|
-
$("addr").textContent = V.STATUS_URL;
|
|
276
160
|
$("mbridge-ask").textContent = V.text(lang).mbridgeAsk;
|
|
277
161
|
$("mbridge-restart").textContent = V.text(lang).mbridgeRestart;
|
|
278
162
|
$("mbridge-ask").addEventListener("click", async () => {
|
|
@@ -294,7 +178,14 @@
|
|
|
294
178
|
if (!ok) { b.disabled = false; b.textContent = t.mbridgeRestart; return; }
|
|
295
179
|
b.textContent = t.mbridgeRestartAsked;
|
|
296
180
|
});
|
|
181
|
+
|
|
297
182
|
words();
|
|
298
|
-
await
|
|
299
|
-
|
|
183
|
+
await readOptions();
|
|
184
|
+
paint();
|
|
185
|
+
// mbridge's snapshot moves every fifteen seconds, and reading the
|
|
186
|
+
// options is free
|
|
187
|
+
setInterval(async () => {
|
|
188
|
+
await readOptions();
|
|
189
|
+
paint();
|
|
190
|
+
}, 5000);
|
|
300
191
|
})();
|
package/page/index.html
CHANGED
|
@@ -12,41 +12,9 @@
|
|
|
12
12
|
<main class="root">
|
|
13
13
|
<header class="head">
|
|
14
14
|
<h1 data-t="title">mbridge</h1>
|
|
15
|
-
<p class="subtitle"
|
|
16
|
-
<p class="since" id="since"></p>
|
|
15
|
+
<p class="subtitle"></p>
|
|
17
16
|
</header>
|
|
18
17
|
|
|
19
|
-
<div class="bar">
|
|
20
|
-
<div class="segmented" id="periods">
|
|
21
|
-
<button type="button" data-period="today" aria-selected="false"></button>
|
|
22
|
-
<button type="button" data-period="7d" aria-selected="false"></button>
|
|
23
|
-
<button type="button" data-period="30d" aria-selected="false"></button>
|
|
24
|
-
</div>
|
|
25
|
-
<div class="segmented" id="sorts">
|
|
26
|
-
<button type="button" data-sort="tokens" aria-selected="false"></button>
|
|
27
|
-
<button type="button" data-sort="calls" aria-selected="false"></button>
|
|
28
|
-
<button type="button" data-sort="cost" aria-selected="false"></button>
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
31
|
-
|
|
32
|
-
<section class="tiles" id="tiles"></section>
|
|
33
|
-
|
|
34
|
-
<section class="models">
|
|
35
|
-
<h2 data-t="models">by model</h2>
|
|
36
|
-
<table class="table">
|
|
37
|
-
<thead>
|
|
38
|
-
<tr>
|
|
39
|
-
<th data-t="model">model</th>
|
|
40
|
-
<th class="num" data-t="calls">calls</th>
|
|
41
|
-
<th class="num" data-t="tokens">tokens</th>
|
|
42
|
-
<th class="num" data-t="cost">cost</th>
|
|
43
|
-
</tr>
|
|
44
|
-
</thead>
|
|
45
|
-
<tbody id="rows"></tbody>
|
|
46
|
-
</table>
|
|
47
|
-
<p class="muted empty" id="empty" hidden></p>
|
|
48
|
-
</section>
|
|
49
|
-
|
|
50
18
|
<section class="mbridge">
|
|
51
19
|
<div class="mbridge-head">
|
|
52
20
|
<h2 data-t="mbridge">mbridge</h2>
|
|
@@ -74,7 +42,6 @@
|
|
|
74
42
|
</section>
|
|
75
43
|
|
|
76
44
|
<section class="notes">
|
|
77
|
-
<p class="note" data-t="disclaimer"></p>
|
|
78
45
|
<p class="note">
|
|
79
46
|
<span id="status"></span>
|
|
80
47
|
<span class="addr" id="addr" title="select and copy it"></span>
|
package/page/view.js
CHANGED
|
@@ -12,25 +12,9 @@
|
|
|
12
12
|
en: {
|
|
13
13
|
title: "mbridge",
|
|
14
14
|
subtitle: "what magpie's ledger recorded",
|
|
15
|
-
calls: "calls",
|
|
16
|
-
errors: "errors",
|
|
17
|
-
errorShare: "of the calls",
|
|
18
|
-
tokens: "tokens",
|
|
19
|
-
cost: "cost",
|
|
20
|
-
models: "by model",
|
|
21
|
-
model: "model",
|
|
22
15
|
today: "today",
|
|
23
16
|
"7d": "7 days",
|
|
24
17
|
"30d": "30 days",
|
|
25
|
-
sortTokens: "tokens",
|
|
26
|
-
sortCalls: "calls",
|
|
27
|
-
sortCost: "cost",
|
|
28
|
-
empty: "no calls in this period",
|
|
29
|
-
failed: "couldn't read usage",
|
|
30
|
-
retry: "try again in a minute",
|
|
31
|
-
since: "since",
|
|
32
|
-
disclaimer:
|
|
33
|
-
"This is everything magpie recorded. It is mbridge's usage only when every client goes through mbridge; whatever you use magpie for directly is in here too.",
|
|
34
18
|
status: "mbridge's own status:",
|
|
35
19
|
restart: "restart it with",
|
|
36
20
|
mbridge: "mbridge",
|
|
@@ -63,25 +47,9 @@
|
|
|
63
47
|
zh: {
|
|
64
48
|
title: "mbridge",
|
|
65
49
|
subtitle: "magpie 记下的全部用量",
|
|
66
|
-
calls: "调用",
|
|
67
|
-
errors: "错误",
|
|
68
|
-
errorShare: "占调用",
|
|
69
|
-
tokens: "Token",
|
|
70
|
-
cost: "费用",
|
|
71
|
-
models: "按模型",
|
|
72
|
-
model: "模型",
|
|
73
50
|
today: "今天",
|
|
74
51
|
"7d": "7 天",
|
|
75
52
|
"30d": "30 天",
|
|
76
|
-
sortTokens: "Token",
|
|
77
|
-
sortCalls: "调用",
|
|
78
|
-
sortCost: "费用",
|
|
79
|
-
empty: "这个区间没有调用",
|
|
80
|
-
failed: "读不到 magpie 的用量",
|
|
81
|
-
retry: "一分钟后自动重试",
|
|
82
|
-
since: "起",
|
|
83
|
-
disclaimer:
|
|
84
|
-
"这是 magpie 记下的全部用量。客户端都走 mbridge 时它才等于 mbridge 的用量;你直接用 magpie 的那部分也在里面。",
|
|
85
53
|
status: "mbridge 自己的状态:",
|
|
86
54
|
restart: "重启用",
|
|
87
55
|
mbridge: "mbridge",
|
|
@@ -113,8 +81,6 @@
|
|
|
113
81
|
},
|
|
114
82
|
};
|
|
115
83
|
|
|
116
|
-
const PERIODS = ["today", "7d", "30d"];
|
|
117
|
-
const SORTS = ["tokens", "calls", "cost"];
|
|
118
84
|
const STATUS_URL = "http://127.0.0.1:3426/status";
|
|
119
85
|
|
|
120
86
|
// text is the words for a language: zh for anything that starts with zh.
|
|
@@ -153,65 +119,13 @@
|
|
|
153
119
|
return (Math.round(v * 10) / 10).toString() + "%";
|
|
154
120
|
}
|
|
155
121
|
|
|
156
|
-
// tiles are the four numbers at the top: calls, errors, tokens, cost.
|
|
157
|
-
function tiles(summary, lang) {
|
|
158
|
-
const t = text(lang);
|
|
159
|
-
const s = summary || {};
|
|
160
|
-
return [
|
|
161
|
-
{ key: "calls", label: t.calls, value: count(s.calls) },
|
|
162
|
-
{
|
|
163
|
-
key: "errors",
|
|
164
|
-
label: t.errors,
|
|
165
|
-
value: count(s.errors),
|
|
166
|
-
sub: t.errorShare + " " + pct(s.errors, s.calls),
|
|
167
|
-
},
|
|
168
|
-
{ key: "tokens", label: t.tokens, value: count((s.input || 0) + (s.output || 0)) },
|
|
169
|
-
{ key: "cost", label: t.cost, value: money(s.cost) },
|
|
170
|
-
];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// rows are the model table, sorted by tokens, calls or cost, with each
|
|
174
|
-
// model's share of the total.
|
|
175
|
-
function rows(summary, sort, lang) {
|
|
176
|
-
const s = summary || {};
|
|
177
|
-
const models = Array.isArray(s.models) ? s.models : [];
|
|
178
|
-
const key = sort === "calls" ? "calls" : sort === "cost" ? "cost" : "tokens";
|
|
179
|
-
const value = (m) => (key === "tokens" ? (m.input || 0) + (m.output || 0) : m[key] || 0);
|
|
180
|
-
const sorted = models.slice().sort((a, b) => value(b) - value(a));
|
|
181
|
-
const total = sorted.reduce((a, m) => a + value(m), 0);
|
|
182
|
-
return sorted.map((m) => ({
|
|
183
|
-
model: m.model || "?",
|
|
184
|
-
calls: count(m.calls),
|
|
185
|
-
tokens: count((m.input || 0) + (m.output || 0)),
|
|
186
|
-
cost: money(m.cost),
|
|
187
|
-
share: pct(value(m), total),
|
|
188
|
-
shareValue: total > 0 ? value(m) / total : 0,
|
|
189
|
-
}));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// since is when the period started.
|
|
193
|
-
function since(summary, lang) {
|
|
194
|
-
const t = text(lang);
|
|
195
|
-
const s = summary || {};
|
|
196
|
-
return s.since ? t.since + " " + s.since : "";
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// periodLabel is the name of a period.
|
|
200
|
-
function periodLabel(period, lang) {
|
|
201
|
-
return text(lang)[period] || period;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// sortLabel is the name of a sort.
|
|
205
|
-
function sortLabel(sort, lang) {
|
|
206
|
-
const t = text(lang);
|
|
207
|
-
return t["sort" + sort.charAt(0).toUpperCase() + sort.slice(1)] || sort;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
122
|
// mbridgeView is what the page shows of mbridge, from the snapshot
|
|
211
123
|
// mbridge published into the plugin's options (SP14 §7).
|
|
212
124
|
function mbridgeView(snapshot, lang) {
|
|
213
125
|
const t = text(lang);
|
|
214
|
-
|
|
126
|
+
// nothing published, or an object that is not a snapshot: an empty
|
|
127
|
+
// options object is not a snapshot, it is no snapshot
|
|
128
|
+
if (!snapshot || (!snapshot.seq && !snapshot.version)) {
|
|
215
129
|
return { none: true, text: t.mbridgeNone };
|
|
216
130
|
}
|
|
217
131
|
const s = snapshot;
|
|
@@ -276,18 +190,8 @@
|
|
|
276
190
|
|
|
277
191
|
globalThis.MbridgeView = {
|
|
278
192
|
TEXT,
|
|
279
|
-
PERIODS,
|
|
280
|
-
SORTS,
|
|
281
193
|
STATUS_URL,
|
|
282
194
|
text,
|
|
283
|
-
count,
|
|
284
|
-
money,
|
|
285
|
-
pct,
|
|
286
|
-
tiles,
|
|
287
|
-
rows,
|
|
288
|
-
since,
|
|
289
|
-
periodLabel,
|
|
290
|
-
sortLabel,
|
|
291
195
|
mbridgeView,
|
|
292
196
|
};
|
|
293
197
|
})();
|