mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
549e6349f8
* fix(workboard): make ownership updates atomic Persist launch intent before worker acceptance, reconcile accepted identities, fence cross-host card mutations, and deduplicate session capture in SQLite. Closes #125932 * test(workboard): keep race fixtures plugin-local Use a plugin-local synchronization helper and update the sessions-page capture expectation for the new store-owned RPC. * fix(workboard): fence lifecycle and restore races Apply terminal lifecycle state only while the matched association is current, and converge concurrent archived session restores on the active winner.
580 lines
15 KiB
TypeScript
580 lines
15 KiB
TypeScript
// Workboard plugin module implements gateway behavior.
|
|
import type { OpenClawPluginApi } from "../api.js";
|
|
import { redactClaimToken } from "./card-redaction.js";
|
|
import {
|
|
assertNoCursorAdvance,
|
|
createWorkboardDispatchHandler,
|
|
listWorkboardCards,
|
|
readId,
|
|
respondError,
|
|
} from "./gateway-helpers.js";
|
|
import {
|
|
registerWorkboardWorkspaceBoardMethod,
|
|
registerWorkboardWorkspaceBulkMethod,
|
|
registerWorkboardWorkspaceCardMethods,
|
|
registerWorkboardWorkspaceWorkflowMethods,
|
|
} from "./gateway-workspace-methods.js";
|
|
import { WorkboardStore } from "./store.js";
|
|
|
|
const READ_SCOPE = "operator.read" as const;
|
|
const WRITE_SCOPE = "operator.write" as const;
|
|
|
|
function redactDiagnosticsRows(result: Awaited<ReturnType<WorkboardStore["diagnostics"]>>) {
|
|
return {
|
|
...result,
|
|
diagnostics: result.diagnostics.map((row) => ({
|
|
...row,
|
|
card: redactClaimToken(row.card),
|
|
})),
|
|
};
|
|
}
|
|
|
|
export function registerWorkboardGatewayMethods(params: {
|
|
api: OpenClawPluginApi;
|
|
store?: WorkboardStore;
|
|
}) {
|
|
const { api } = params;
|
|
const store = params.store ?? WorkboardStore.openSqlite();
|
|
const dispatchCards = createWorkboardDispatchHandler({
|
|
api,
|
|
store,
|
|
redactCard: redactClaimToken,
|
|
});
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.list",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, await listWorkboardCards(store, requestParams.boardId, redactClaimToken));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
|
|
registerWorkboardWorkspaceCardMethods({ api, store, redactCard: redactClaimToken });
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.start",
|
|
async (context) => await dispatchCards(context, { supportsMaxStarts: false, directCard: true }),
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.move",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(
|
|
await store.move(readId(requestParams), requestParams.status, requestParams.position),
|
|
),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.delete",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, await store.delete(readId(requestParams)));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.comment",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.addComment(readId(requestParams), requestParams)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.link",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.addLink(readId(requestParams), requestParams)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.linkDependency",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
const parentId = requestParams.parentId;
|
|
const childId = requestParams.childId;
|
|
if (typeof parentId !== "string" || typeof childId !== "string") {
|
|
throw new Error("parentId and childId are required.");
|
|
}
|
|
respond(true, {
|
|
card: redactClaimToken(await store.linkCards(parentId, childId)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.proof",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.addProof(readId(requestParams), requestParams)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.artifact",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.addArtifact(readId(requestParams), requestParams)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.claim",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
const claimed = await store.claim(readId(requestParams), requestParams);
|
|
respond(true, { ...claimed, card: redactClaimToken(claimed.card) });
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.heartbeat",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.heartbeat(readId(requestParams), requestParams)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.release",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.releaseClaim(readId(requestParams), requestParams)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.promote",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.promote(readId(requestParams), requestParams, null)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.reassign",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.reassign(readId(requestParams), requestParams, null)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.reclaim",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.reclaim(readId(requestParams), requestParams, null)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.complete",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.complete(readId(requestParams), requestParams, null)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.block",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.block(readId(requestParams), requestParams, null)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.unblock",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.unblock(readId(requestParams))),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
registerWorkboardWorkspaceBulkMethod({ api, store, redactCard: redactClaimToken });
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.diagnostics",
|
|
async ({ respond }) => {
|
|
try {
|
|
respond(true, redactDiagnosticsRows(await store.diagnostics()));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.diagnostics.refresh",
|
|
async ({ respond }) => {
|
|
try {
|
|
respond(true, redactDiagnosticsRows(await store.refreshDiagnostics()));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.dispatch",
|
|
async (context) => await dispatchCards(context, { supportsMaxStarts: false }),
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.dispatchWithOptions",
|
|
async (context) => await dispatchCards(context, { supportsMaxStarts: true }),
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.boards.list",
|
|
async ({ respond }) => {
|
|
try {
|
|
respond(true, await store.listBoards());
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
|
|
registerWorkboardWorkspaceBoardMethod({ api, store, redactCard: redactClaimToken });
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.boards.archive",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
board: await store.archiveBoard(requestParams.id, requestParams.archived),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.boards.delete",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, await store.deleteBoard(requestParams.id));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.stats",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, await store.stats({ boardId: requestParams.boardId }));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.runs",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
const result = await store.runs(readId(requestParams));
|
|
respond(true, { ...result, card: redactClaimToken(result.card) });
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
|
|
registerWorkboardWorkspaceWorkflowMethods({ api, store, redactCard: redactClaimToken });
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.notifications.subscribe",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, { subscription: await store.subscribeNotifications(requestParams) });
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.notifications.list",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, await store.listNotificationSubscriptions(requestParams));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.notifications.delete",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, await store.deleteNotificationSubscription(readId(requestParams)));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.notifications.events",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
assertNoCursorAdvance(requestParams);
|
|
respond(true, await store.notificationEvents(requestParams));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.notifications.advance",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, await store.advanceNotificationEvents(requestParams));
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.attachments.list",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
const result = await store.listAttachments(readId(requestParams));
|
|
respond(true, { ...result, card: redactClaimToken(result.card) });
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.attachments.get",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
const attachment = await store.getAttachment(readId(requestParams));
|
|
if (!attachment) {
|
|
throw new Error(`attachment not found: ${readId(requestParams)}`);
|
|
}
|
|
respond(true, attachment);
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.attachments.add",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.addAttachment(readId(requestParams), requestParams)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.attachments.delete",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
const attachmentId = requestParams.attachmentId;
|
|
if (typeof attachmentId !== "string" || !attachmentId.trim()) {
|
|
throw new Error("attachmentId is required.");
|
|
}
|
|
respond(true, {
|
|
card: redactClaimToken(
|
|
await store.deleteAttachment(readId(requestParams), attachmentId.trim()),
|
|
),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.workerLog",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(await store.addWorkerLog(readId(requestParams), requestParams)),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.protocolViolation",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(
|
|
await store.recordProtocolViolation(readId(requestParams), requestParams),
|
|
),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.archive",
|
|
async ({ params: requestParams, respond }) => {
|
|
try {
|
|
respond(true, {
|
|
card: redactClaimToken(
|
|
await store.archive(readId(requestParams), requestParams.archived),
|
|
),
|
|
});
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: WRITE_SCOPE },
|
|
);
|
|
|
|
api.registerGatewayMethod(
|
|
"workboard.cards.export",
|
|
async ({ respond }) => {
|
|
try {
|
|
const exported = await store.exportCards();
|
|
respond(true, { ...exported, cards: exported.cards.map(redactClaimToken) });
|
|
} catch (error) {
|
|
respondError(respond, error);
|
|
}
|
|
},
|
|
{ scope: READ_SCOPE },
|
|
);
|
|
}
|