feat(artifacts): capture Claude Docs docs and session documents, claim unowned rows

- A Claude Docs `batch` that creates a doc is captured like an `Artifact`
  publish, titled by the doc's name and linked by its own frame URL.
- At Stop/SessionEnd, each pptx/docx/xlsx/pdf/html a workspace session
  created or changed is snapshotted into its owner's library (`auto`);
  an unchanged file is skipped, a changed one refreshes its snapshot.
- Server mode: a page with no owner carries a suggested owner (the
  workspace or home its session ran in). The suggested user claims or
  declines it; an admin claims or assigns any. New tab Unowned /
  Is this yours? on the Artifacts page; all three actions audited.
This commit is contained in:
2026-10-23 08:33:55 +07:00
parent 241e19b713
commit 62b4520507
14 changed files with 736 additions and 44 deletions
+22 -2
View File
@@ -3016,7 +3016,9 @@ export const api = {
},
/** The Artifacts hub (`/api/artifacts`, `server/lib/artifacts.js`). */
artifacts: {
list: (params: { scope?: "mine" | "hub" | "all"; kind?: ArtifactKind; q?: string } = {}) => {
list: (
params: { scope?: "mine" | "hub" | "all" | "unowned"; kind?: ArtifactKind; q?: string } = {}
) => {
const qs = new URLSearchParams();
if (params.scope) qs.set("scope", params.scope);
if (params.kind) qs.set("kind", params.kind);
@@ -3092,6 +3094,18 @@ export const api = {
for (const [k, v] of Object.entries(fields)) if (v) form.append(k, v);
return request<{ item: Artifact }>("/artifacts/file/upload", { method: "POST", body: form });
},
/** An unowned row: its suggested user (or an admin) takes it. */
claim: (id: number) =>
request<{ item: Artifact }>(`/artifacts/${id}/claim`, { method: "POST" }),
/** An unowned row offered to the caller is not theirs. */
decline: (id: number) =>
request<{ id: number; declined: true }>(`/artifacts/${id}/decline`, { method: "POST" }),
/** Admin: give an unowned row to a user. */
assign: (id: number, userId: number) =>
request<{ item: Artifact }>(`/artifacts/${id}/assign`, {
method: "POST",
body: JSON.stringify({ userId }),
}),
update: (
id: number,
body: {
@@ -4382,7 +4396,7 @@ export interface Artifact {
market: string | null;
/** page: the claude.ai link. */
url: string | null;
files: { path: string; size: number }[];
files: { path: string; size: number; mtime?: number }[];
size: number | null;
shared: boolean;
sharedAt: string | null;
@@ -4391,6 +4405,12 @@ export interface Artifact {
inLibrary: boolean;
/** List rows only: a skill/plugin already in every account the viewer's sessions run as. */
installed?: boolean;
/** file: captured when its session stopped, not pinned by hand. */
auto?: boolean;
/** Server mode: nobody owns it yet. */
unowned?: boolean;
/** Unowned and offered to the caller to claim or decline. */
suggestedForMe?: boolean;
createdAt: string;
updatedAt: string;
}