feat(desktop): implement workspace foundation + desktop-client skeleton

Plan 0 (workspaces): add workspaces + workspace_memberships schema, extend
JWT/Actor/claims with primary_workspace_id, seed default workspace per tenant,
thread workspace_id through tenant monitoring quota.

Plan A (desktop skeleton): new Electron app (apps/desktop-client) with main/
preload/renderer, shared Vue component package (packages/ui-shared), and server
surface — desktop client registration + token rotation + heartbeat, SSE task
event stream, desktop accounts/tasks/content handlers, publish job endpoint,
and supporting repositories, services, sqlc queries, and migrations.

Hard cutover per plan: remove browser-extension monitoring callback endpoints,
stub legacy media API in admin-web, and delete monitoring_callback_handler.go.
This commit is contained in:
2026-04-19 14:18:20 +08:00
parent 98f9e95875
commit b16e9f0bd1
141 changed files with 21533 additions and 357 deletions
@@ -1,5 +1,4 @@
import type {
ApiEnvelope,
JsonValue,
MonitoringHeartbeatPlatformPayload,
MonitoringHeartbeatResponse,
@@ -161,30 +160,9 @@ function toMonitoringPlatformPayload(platform: StoredMonitoringPlatformState): M
}
async function postMonitoringApi<T>(path: string, payload: unknown): Promise<T> {
const state = await getExtensionState();
if (!state.api_base_url) {
throw new Error("monitoring_api_base_url_missing");
}
if (!state.plugin_installation_id || !state.installation_token) {
throw new Error("monitoring_installation_credentials_missing");
}
const response = await fetch(new URL(path.replace(/^\//, ""), normalizeBaseUrl(state.api_base_url)).toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Geo-Installation-Id": String(state.plugin_installation_id),
"X-Geo-Installation-Token": state.installation_token,
},
body: JSON.stringify(payload),
});
const body = (await response.json()) as ApiEnvelope<T> | { message?: string };
if (!response.ok || !("data" in body)) {
throw new Error(body.message || "monitoring_plugin_request_failed");
}
return body.data;
void path;
void payload;
throw new Error("browser_extension_monitoring_removed_use_desktop_client");
}
async function detectAllMonitoringPlatforms(): Promise<StoredMonitoringPlatformState[]> {
@@ -255,11 +233,12 @@ async function markMonitoringTaskSkipped(task: MonitoringLeaseTask, reason: stri
error_message: normalizeText(errorMessage),
completed_at: new Date().toISOString(),
};
return await postMonitoringApi<MonitoringSkipTaskResponse>(`/api/plugin/monitoring/tasks/${task.task_id}/skip`, payload);
return await postMonitoringApi<MonitoringSkipTaskResponse>("skip_task", payload);
}
async function submitMonitoringTaskResult(task: MonitoringLeaseTask, payload: MonitoringTaskResultPayload): Promise<MonitoringTaskResultResponse> {
return await postMonitoringApi<MonitoringTaskResultResponse>(`/api/plugin/monitoring/tasks/${task.task_id}/result`, payload);
void task;
return await postMonitoringApi<MonitoringTaskResultResponse>("submit_result", payload);
}
async function processMonitoringLeasedTask(
@@ -290,7 +269,7 @@ async function resumeMonitoringTasks(aiPlatformIDs: string[]): Promise<Monitorin
const payload: MonitoringResumeTasksPayload = {
ai_platform_ids: aiPlatformIDs,
};
const resumed = await postMonitoringApi<MonitoringResumeTasksResponse>("/api/plugin/monitoring/tasks/resume", payload);
const resumed = await postMonitoringApi<MonitoringResumeTasksResponse>("resume_tasks", payload);
return resumed.tasks;
}
@@ -299,7 +278,7 @@ async function leaseMonitoringTasks(aiPlatformIDs: string[]): Promise<Monitoring
limit: monitoringCycleBatchLeaseLimit,
ai_platform_ids: aiPlatformIDs,
};
return await postMonitoringApi<MonitoringLeaseTasksResponse>("/api/plugin/monitoring/tasks/lease", payload);
return await postMonitoringApi<MonitoringLeaseTasksResponse>("lease_tasks", payload);
}
function mergeMonitoringQueueTasks(
@@ -580,7 +559,7 @@ async function handleMonitoringSyncHeartbeat(allowCachedRemote: boolean): Promis
}
try {
const heartbeat = await postMonitoringApi<MonitoringHeartbeatResponse>("/api/plugin/monitoring/heartbeat", {
const heartbeat = await postMonitoringApi<MonitoringHeartbeatResponse>("heartbeat", {
platforms: persistedPlatforms.map(toMonitoringPlatformPayload),
});