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
+29
View File
@@ -38,6 +38,7 @@ const STATUS = {
EMARKETFETCH: 502,
ENEEDLOGIN: 502,
ETOOBIG: 413,
EOWNED: 409,
};
function sendError(res, err) {
@@ -362,6 +363,34 @@ router.post(
})
);
// A row nobody owns: its suggested user says "mine" or "not mine"; an admin
// takes it or gives it to someone (artifacts.js `claim`/`decline`/`assign`).
router.post(
"/:id/claim",
sameOriginGuard,
wrap((req, res) => {
const item = artifacts.claim(req, req.params.id);
changed(item);
res.json({ item });
})
);
router.post(
"/:id/decline",
sameOriginGuard,
wrap((req, res) => res.json(artifacts.decline(req, req.params.id)))
);
router.post(
"/:id/assign",
sameOriginGuard,
wrap((req, res) => {
const item = artifacts.assign(req, req.params.id, req.body?.userId);
changed(item);
res.json({ item });
})
);
router.post(
"/:id/uninstall",
sameOriginGuard,
+23 -2
View File
@@ -1295,9 +1295,13 @@ const processEventTx = db.transaction((hookType, data, tool = "claude", peer = n
};
broadcast("new_event", event);
// A claude.ai page the session published goes into its owner's Artifacts.
if (hookType === "PostToolUse" && toolName === "Artifact" && tool !== "codex") {
// A Claude Docs doc counts too (`isPageTool`): it is a claude.ai page the session made.
if (hookType === "PostToolUse" && tool !== "codex") {
try {
const row = require("../lib/artifacts").captureFromHook(sessionId, data, peer);
const artifacts = require("../lib/artifacts");
const row = artifacts.isPageTool(toolName)
? artifacts.captureFromHook(sessionId, data, peer)
: null;
if (row) {
broadcast("artifact_changed", { id: row.id, ownerId: row.owner_id, shared: false });
}
@@ -1736,6 +1740,23 @@ router.post("/event", (req, res) => {
});
}
// Documents the session made (slides, Word, Excel, PDF, HTML) go into its
// workspace owner's Artifacts when it stops. Off the response path, fail-safe.
if (["Stop", "SessionEnd"].includes(hook_type) && data.session_id) {
require("../lib/artifacts")
.captureOutputs(data.session_id)
.then((rows) => {
for (const row of rows) {
broadcast("artifact_changed", {
id: row.id,
ownerId: row.owner_id,
shared: row.shared === 1,
});
}
})
.catch((err) => console.warn("[artifacts] output capture failed:", err.message));
}
// Ingest Workflow-tool run journals from disk. Inner agent() calls emit NO
// hooks, so the journal (written at workflow completion) is the only source.
// Run on the lifecycle hooks that bracket a workflow finishing, off the