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
+48 -7
View File
@@ -314,11 +314,16 @@ func (s *MediaService) RegisterPluginInstallation(ctx context.Context, req Regis
func ensureMonitoringPrimaryInstallation(ctx context.Context, tx pgx.Tx, tenantID, installationID int64) error {
if _, err := tx.Exec(ctx, `
INSERT INTO tenant_monitoring_quotas (
tenant_id, primary_installation_id
tenant_id, workspace_id, primary_installation_id
)
VALUES (
$1,
(SELECT id FROM workspaces WHERE tenant_id = $1 AND is_default = TRUE LIMIT 1),
$2
)
VALUES ($1, $2)
ON CONFLICT (tenant_id) DO UPDATE
SET primary_installation_id = COALESCE(tenant_monitoring_quotas.primary_installation_id, EXCLUDED.primary_installation_id),
SET workspace_id = COALESCE(tenant_monitoring_quotas.workspace_id, EXCLUDED.workspace_id),
primary_installation_id = COALESCE(tenant_monitoring_quotas.primary_installation_id, EXCLUDED.primary_installation_id),
updated_at = NOW()
`, tenantID, installationID); err != nil {
return response.ErrInternal(50068, "monitoring_quota_update_failed", "failed to assign monitoring plugin installation")
@@ -652,7 +657,11 @@ func (s *MediaService) HandleBindCallback(ctx context.Context, req PluginBindCal
err = tx.QueryRow(ctx, `
SELECT id
FROM platform_accounts
WHERE tenant_id = $1 AND platform_id = $2 AND platform_uid = $3 AND deleted_at IS NULL
WHERE tenant_id = $1
AND workspace_id = (SELECT id FROM workspaces WHERE tenant_id = $1 AND is_default = TRUE LIMIT 1)
AND platform_id = $2
AND platform_uid = $3
AND deleted_at IS NULL
LIMIT 1
`, session.TenantID, req.PlatformID, req.PlatformUID).Scan(&platformAccountID)
@@ -660,10 +669,29 @@ func (s *MediaService) HandleBindCallback(ctx context.Context, req PluginBindCal
case errors.Is(err, pgx.ErrNoRows):
if err := tx.QueryRow(ctx, `
INSERT INTO platform_accounts (
tenant_id, user_id, platform_id, platform_uid, nickname, avatar_url, status,
metadata_json, last_check_at
tenant_id, workspace_id, user_id, platform_id, platform_uid, nickname, avatar_url, status,
metadata_json, last_check_at, desktop_id, client_id, account_fingerprint,
display_name, health, verified_at, tags, sync_version
)
VALUES (
$1,
(SELECT id FROM workspaces WHERE tenant_id = $1 AND is_default = TRUE LIMIT 1),
$2, $3, $4, $5, $6, $7, $8, NOW(),
gen_random_uuid(), NULL, NULL,
$5,
CASE
WHEN $7 = 'active' THEN 'live'
WHEN $7 = 'captcha' THEN 'captcha'
WHEN $7 = 'risk' THEN 'risk'
ELSE 'expired'
END,
NOW(),
CASE
WHEN $8 IS NOT NULL AND jsonb_typeof($8 -> 'tags') = 'array' THEN $8 -> 'tags'
ELSE NULL
END,
1
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())
RETURNING id
`, session.TenantID, session.UserID, req.PlatformID, req.PlatformUID, req.Nickname, req.AvatarURL, status, metadataJSON).Scan(&platformAccountID); err != nil {
return nil, response.ErrInternal(50045, "platform_account_insert_failed", "failed to create platform account")
@@ -675,10 +703,23 @@ func (s *MediaService) HandleBindCallback(ctx context.Context, req PluginBindCal
UPDATE platform_accounts
SET user_id = $1,
nickname = $2,
display_name = $2,
avatar_url = $3,
status = $4,
health = CASE
WHEN $4 = 'active' THEN 'live'
WHEN $4 = 'captcha' THEN 'captcha'
WHEN $4 = 'risk' THEN 'risk'
ELSE 'expired'
END,
metadata_json = $5,
last_check_at = NOW(),
verified_at = NOW(),
tags = CASE
WHEN $5 IS NOT NULL AND jsonb_typeof($5 -> 'tags') = 'array' THEN $5 -> 'tags'
ELSE tags
END,
sync_version = sync_version + 1,
updated_at = NOW()
WHERE id = $6
`, session.UserID, req.Nickname, req.AvatarURL, status, metadataJSON, platformAccountID); err != nil {