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
+30 -15
View File
@@ -21,14 +21,6 @@ func RegisterRoutes(app *bootstrap.App) {
callbacks.POST("/bind", pluginHandler.PluginBindCallback)
callbacks.POST("/publish", pluginHandler.PluginPublishCallback)
monitoringCallbackHandler := NewMonitoringCallbackHandler(app)
monitoringPlugin := app.Engine.Group("/api/plugin/monitoring")
monitoringPlugin.POST("/heartbeat", monitoringCallbackHandler.Heartbeat)
monitoringPlugin.POST("/tasks/lease", monitoringCallbackHandler.LeaseTasks)
monitoringPlugin.POST("/tasks/resume", monitoringCallbackHandler.ResumeTasks)
monitoringPlugin.POST("/tasks/:id/result", monitoringCallbackHandler.TaskResult)
monitoringPlugin.POST("/tasks/:id/skip", monitoringCallbackHandler.SkipTask)
protected := app.Engine.Group("/api")
protected.Use(auth.Middleware(app.JWT, app.Sessions))
protected.Use(middleware.TenantScope())
@@ -36,8 +28,38 @@ func RegisterRoutes(app *bootstrap.App) {
protected.GET("/auth/me", authHandler.Me)
protected.POST("/auth/logout", authHandler.Logout)
desktopClientHandler := NewDesktopClientHandler(app)
desktopAccountHandler := NewDesktopAccountHandler(app)
desktopTaskHandler := NewDesktopTaskHandler(app)
desktopEventsHandler := NewDesktopEventsHandler(app)
desktopContentHandler := NewDesktopContentHandler(app)
publishJobHandler := NewPublishJobHandler(app)
protected.POST("/desktop/clients/register", desktopClientHandler.Register)
desktopAuth := app.Engine.Group("/api/desktop")
desktopAuth.Use(DesktopClientMiddleware(repository.NewDesktopClientRepository(app.DB)))
desktopAuth.POST("/clients/rotate", desktopClientHandler.Rotate)
desktopAuth.POST("/clients/heartbeat", desktopClientHandler.Heartbeat)
desktopAuth.POST("/clients/revoke", desktopClientHandler.Revoke)
desktopAuth.GET("/events", desktopEventsHandler.Stream)
desktopAuth.GET("/accounts", desktopAccountHandler.List)
desktopAuth.GET("/content/articles/:id", desktopContentHandler.Article)
desktopAuth.POST("/accounts", desktopAccountHandler.Upsert)
desktopAuth.PATCH("/accounts/:id", desktopAccountHandler.Patch)
desktopAuth.DELETE("/accounts/:id", desktopAccountHandler.Delete)
desktopAuth.POST("/tasks/lease", desktopTaskHandler.Lease)
desktopAuth.POST("/tasks/:id/lease", desktopTaskHandler.Lease)
desktopAuth.POST("/tasks/:id/extend", desktopTaskHandler.Extend)
desktopAuth.POST("/tasks/:id/park", desktopTaskHandler.Park)
desktopAuth.POST("/tasks/:id/cancel", desktopTaskHandler.Cancel)
desktopAuth.POST("/tasks/:id/result", desktopTaskHandler.Result)
tenantProtected := protected.Group("/tenant")
tenantProtected.Use(RequireActiveTenantSubscription(repository.NewTenantPlanRepository(app.DB)))
tenantProtected.POST("/accounts/:id/request-delete", desktopAccountHandler.RequestDelete)
tenantProtected.POST("/tasks/:id/reconcile", desktopTaskHandler.Reconcile)
tenantProtected.POST("/tasks/:id/cancel", desktopTaskHandler.TenantCancel)
tenantProtected.POST("/publish-jobs", publishJobHandler.Create)
workspace := tenantProtected.Group("/workspace")
wsHandler := NewWorkspaceHandler(app)
@@ -125,13 +147,6 @@ func RegisterRoutes(app *bootstrap.App) {
articles.POST("/:id/publish-batches", pluginHandler.CreatePublishBatch)
articles.DELETE("/:id", artHandler.Delete)
media := tenantProtected.Group("/media")
media.GET("/platforms", pluginHandler.ListPlatforms)
media.GET("/platform-accounts", pluginHandler.ListPlatformAccounts)
media.POST("/plugin-installations/register", pluginHandler.RegisterPluginInstallation)
media.POST("/plugin-sessions", pluginHandler.CreatePluginSession)
media.DELETE("/platform-accounts/:id", pluginHandler.DeletePlatformAccount)
brands := tenantProtected.Group("/brands")
brandHandler := NewBrandHandler(app)
brands.GET("", brandHandler.List)