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
@@ -0,0 +1,105 @@
import { Modal } from "ant-design-vue";
type ClientErrorTone = "error" | "warning";
type ClientActionKind = "bind-account" | "open-console";
interface ClientErrorPresentation {
tone: ClientErrorTone;
title: string;
content: string;
}
const REMOTE_METHOD_PREFIX = /^Error invoking remote method '[^']+':\s*/i;
const ERROR_PREFIX = /^Error:\s*/i;
function rawErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === "string") {
return error;
}
return "";
}
export function unwrapClientErrorMessage(error: unknown, fallback: string): string {
let message = rawErrorMessage(error).trim();
let previous = "";
while (message && message !== previous) {
previous = message;
message = message
.replace(REMOTE_METHOD_PREFIX, "")
.replace(ERROR_PREFIX, "")
.trim();
}
return message || fallback;
}
function presentClientError(kind: ClientActionKind, error: unknown): ClientErrorPresentation {
const message = unwrapClientErrorMessage(
error,
kind === "bind-account" ? "账号绑定失败" : "打开创作台失败",
);
if (message === "desktop_account_bind_window_closed") {
return {
tone: "warning",
title: "授权已中断",
content: "授权窗口已关闭,本次绑定没有完成。重新点击“绑定账号”即可继续。",
};
}
if (message === "desktop_account_upsert_failed") {
return {
tone: "error",
title: "保存授权账号失败",
content: "平台侧授权已经完成,但客户端回写账号信息失败。请稍后重试。",
};
}
if (message === "desktop_account_bind_failed") {
return {
tone: "error",
title: "账号绑定失败",
content: "授权流程执行失败,请稍后重新发起绑定。",
};
}
if (message.startsWith("desktop_publish_platform_not_supported:")) {
const platformId = message.split(":")[1] || "当前平台";
return {
tone: "error",
title: "平台暂不支持",
content: `${platformId} 暂时还不支持这个操作。`,
};
}
if (kind === "open-console") {
return {
tone: "error",
title: "打开创作台失败",
content: message,
};
}
return {
tone: "error",
title: "账号绑定失败",
content: message,
};
}
export function showClientActionError(kind: ClientActionKind, error: unknown): void {
const presentation = presentClientError(kind, error);
const showModal = presentation.tone === "warning" ? Modal.warning : Modal.error;
showModal({
title: presentation.title,
content: presentation.content,
okText: "知道了",
centered: true,
maskClosable: true,
});
}