feat: v2.8.0 — Artifacts uploads and plugin links, console tab combos, account avatars

- Artifacts hub: upload a file from the computer; make a skill by uploading a
  Markdown file, files, a folder or a .zip/.skill, or by writing it in the
  dialog; add plugins by picking from the list a marketplace link answers
  (POST /api/artifacts/plugin/inspect); Settings → AI tools → Artifacts Hub.
- Console tabs are combos of up to four split panes (ConsoleTabs, PaneGrid).
- Accounts: an own avatar (PUT/DELETE /api/me/avatar, raster only, ≤ 1 MB,
  <data dir>/avatars/), shown with name and role at the sidebar's foot and on
  the workspace owner groups (GET /api/users/:id/avatar, owner_avatar_at).
- Users page: per-user storage (GET /api/users/storage, measured in a worker)
  and account-pool spend/tokens per account (server/lib/pool-usage.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 17:21:33 +07:00
parent 8ed5018a2f
commit 7a88b74016
63 changed files with 3833 additions and 354 deletions
+35 -5
View File
@@ -287,9 +287,32 @@ const BUCKET_COLS = ["model", "speed", "inference_geo", "service_tier"];
* its session's `tool`, for the per-tool split.
*/
function dailyTokenRows(tzModifier, whereClause, params) {
return bookedTokenRows(
{ total: "DATE(s.started_at, ?)", hour: "DATE(h.hour || ':00:00', ?)", args: [tzModifier] },
whereClause,
params
);
}
/**
* The same booking per UTC hour (`YYYY-MM-DDTHH` in `date`) and per session
* (`session_id`): what the account pool sums over a run's own hours. The rest
* of a total goes to the session's start hour.
*/
function hourlyTokenRows(whereClause, params) {
return bookedTokenRows(
{ total: "substr(s.started_at, 1, 13)", hour: "h.hour", args: [] },
whereClause,
params,
{ bySession: true }
);
}
/** `slot.total`/`slot.hour` name the period of a total and of an hourly row (`slot.args` bound once each). */
function bookedTokenRows(slot, whereClause, params, { bySession = false } = {}) {
const totals = db
.prepare(
`SELECT tu.session_id, DATE(s.started_at, ?) as date, s.tool,
`SELECT tu.session_id, ${slot.total} as date, s.tool,
${BUCKET_COLS.map((c) => `tu.${c}`).join(", ")},
tu.input_tokens + tu.baseline_input as input_tokens,
tu.output_tokens + tu.baseline_output as output_tokens,
@@ -304,10 +327,10 @@ function dailyTokenRows(tzModifier, whereClause, params) {
JOIN sessions s ON s.id = tu.session_id
${whereClause}`
)
.all(tzModifier, ...params);
.all(...slot.args, ...params);
const hourly = db
.prepare(
`SELECT h.session_id, DATE(h.hour || ':00:00', ?) as date,
`SELECT h.session_id, ${slot.hour} as date,
${BUCKET_COLS.map((c) => `h.${c}`).join(", ")},
${USAGE_COLS.map((c) => `SUM(h.${c}) as ${c}`).join(", ")}
FROM token_usage_hourly h
@@ -315,7 +338,7 @@ function dailyTokenRows(tzModifier, whereClause, params) {
${whereClause}
GROUP BY h.session_id, 2, ${BUCKET_COLS.map((c) => `h.${c}`).join(", ")}`
)
.all(tzModifier, ...params);
.all(...slot.args, ...params);
const keyOf = (r) => [r.session_id, ...BUCKET_COLS.map((c) => r[c])].join("\u0001");
const hoursByBucket = new Map();
@@ -327,12 +350,18 @@ function dailyTokenRows(tzModifier, whereClause, params) {
const out = new Map();
const add = (date, bucket, values) => {
const k = [date, bucket.tool, ...BUCKET_COLS.map((c) => bucket[c])].join("\u0001");
const k = [
date,
bucket.tool,
...(bySession ? [bucket.session_id] : []),
...BUCKET_COLS.map((c) => bucket[c]),
].join("\u0001");
let row = out.get(k);
if (!row) {
row = {
date,
tool: bucket.tool,
...(bySession ? { session_id: bucket.session_id } : {}),
...Object.fromEntries(BUCKET_COLS.map((c) => [c, bucket[c]])),
};
for (const c of USAGE_COLS) row[c] = 0;
@@ -697,6 +726,7 @@ function attachAgentCosts(agents) {
module.exports = router;
module.exports.calculateCost = calculateCost;
module.exports.dailyTokenRows = dailyTokenRows;
module.exports.hourlyTokenRows = hourlyTokenRows;
module.exports.costWhere = costWhere;
module.exports.computeCost = computeCost;
module.exports.agentOwnCost = agentOwnCost;