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:
@@ -1,7 +1,9 @@
|
||||
/**
|
||||
* @file artifacts.js (routes)
|
||||
* @description `/api/artifacts` — the Artifacts hub (server/lib/artifacts.js):
|
||||
* list, add (a claude.ai page, a pinned workspace file, a skill, a plugin),
|
||||
* list, add (a claude.ai page, a pinned workspace file or an uploaded one, a
|
||||
* skill — picked, uploaded (file, folder, zip) or written —, a plugin, after
|
||||
* reading its marketplace's plugin list from a link),
|
||||
* rename/share/unshare, delete, read a snapshot file (preview/download, under
|
||||
* a `sandbox` CSP with `nosniff`, so nothing shared runs on the dashboard's
|
||||
* origin), and install a shared skill or plugin into the caller's own
|
||||
@@ -32,6 +34,8 @@ const STATUS = {
|
||||
EUPGRADE: 409,
|
||||
ENOTSHARED: 409,
|
||||
EPLUGIN: 502,
|
||||
EMARKETFETCH: 502,
|
||||
ENEEDLOGIN: 502,
|
||||
ETOOBIG: 413,
|
||||
};
|
||||
|
||||
@@ -152,6 +156,93 @@ router.post(
|
||||
})
|
||||
);
|
||||
|
||||
// Read a marketplace (a git link, owner/repo, or a marketplace.json link)
|
||||
// and list its plugins to pick from. A POST: it reaches the network.
|
||||
router.post(
|
||||
"/plugin/inspect",
|
||||
sameOriginGuard,
|
||||
wrap(async (req, res) => res.json(await artifacts.inspectMarket(req.body?.market)))
|
||||
);
|
||||
|
||||
// A skill written in the dialog: {name, description, content, title?}.
|
||||
router.post(
|
||||
"/skill/new",
|
||||
sameOriginGuard,
|
||||
wrap((req, res) => {
|
||||
const item = artifacts.writeSkill(req, req.body || {});
|
||||
changed(item);
|
||||
res.status(201).json({ item, items: [item] });
|
||||
})
|
||||
);
|
||||
|
||||
/** multer, in memory, for one upload route; errors answered like ours. */
|
||||
function upload(limits) {
|
||||
return (req, res, next) => {
|
||||
const multer = require("multer");
|
||||
multer({ storage: multer.memoryStorage(), limits }).array("files")(req, res, (err) => {
|
||||
if (!err) return next();
|
||||
const big = err.code === "LIMIT_FILE_SIZE" || err.code === "LIMIT_FILE_COUNT";
|
||||
return res.status(big ? 413 : 400).json({
|
||||
error: { code: big ? "ETOOBIG" : "EBADUPLOAD", message: err.message },
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/** Each uploaded file with the relative path the browser sent in `paths` (a folder pick keeps it). */
|
||||
function uploads(req) {
|
||||
let paths = [];
|
||||
try {
|
||||
paths = JSON.parse(req.body?.paths || "[]");
|
||||
} catch {
|
||||
paths = [];
|
||||
}
|
||||
return (req.files || []).map((f, i) => ({
|
||||
path: typeof paths[i] === "string" && paths[i] ? paths[i] : f.originalname,
|
||||
data: f.buffer,
|
||||
}));
|
||||
}
|
||||
|
||||
// Skills from files: one Markdown file, a folder, several files, a .zip/.skill,
|
||||
// or several skills at once (server/lib/artifacts.js `skillsFromUpload`).
|
||||
router.post(
|
||||
"/skill/upload",
|
||||
sameOriginGuard,
|
||||
upload({ files: 400, fileSize: 10 * 1024 * 1024, fields: 10 }),
|
||||
wrap((req, res) => {
|
||||
const b = req.body || {};
|
||||
const items = artifacts.uploadSkills(req, {
|
||||
uploads: uploads(req),
|
||||
name: b.name,
|
||||
title: b.title,
|
||||
description: b.description,
|
||||
});
|
||||
for (const item of items) changed(item);
|
||||
res.status(201).json({ items });
|
||||
})
|
||||
);
|
||||
|
||||
// One file from the user's computer, kept like a pinned workspace file.
|
||||
router.post(
|
||||
"/file/upload",
|
||||
sameOriginGuard,
|
||||
upload({ files: 1, fileSize: 25 * 1024 * 1024, fields: 10 }),
|
||||
wrap((req, res) => {
|
||||
const f = (req.files || [])[0];
|
||||
if (!f) {
|
||||
return res.status(400).json({ error: { code: "ENOFILE", message: "attach the file as `files`" } });
|
||||
}
|
||||
const item = artifacts.uploadFile(req, {
|
||||
name: f.originalname,
|
||||
data: f.buffer,
|
||||
title: req.body?.title,
|
||||
description: req.body?.description,
|
||||
});
|
||||
changed(item);
|
||||
res.status(201).json({ item });
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/plugin",
|
||||
sameOriginGuard,
|
||||
|
||||
Reference in New Issue
Block a user