diff --git a/cmd/desktop/main.go b/cmd/desktop/main.go index 3ad8588b..f36a1482 100644 --- a/cmd/desktop/main.go +++ b/cmd/desktop/main.go @@ -46,6 +46,11 @@ import ( const dragHandlerJS = `(function(){ if(window.__wkDragBound)return; window.__wkDragBound=true; +document.documentElement.classList.add('wails-desktop'); + +// Disable rubber-band overscroll that reveals the dark window background +document.documentElement.style.overscrollBehavior='none'; +document.body.style.overscrollBehavior='none'; if(window.wails&&window.wails.flags){ window.wails.flags.cssDragProperty='__disabled__'; @@ -107,6 +112,32 @@ window.addEventListener('mousedown',function(e){ e.preventDefault(); sendDrag(); },true); + +// Intercept external link clicks and window.open so they open in the system browser +document.addEventListener('click',function(e){ + var el=e.target; + while(el&&el.tagName!=='A')el=el.parentElement; + if(!el||!el.href)return; + var href=el.href; + if(href.indexOf('http://')===0||href.indexOf('https://')===0){ + if(window.runtime&&window.runtime.BrowserOpenURL){ + e.preventDefault(); + e.stopPropagation(); + window.runtime.BrowserOpenURL(href); + } + } +},true); + +var origOpen=window.open; +window.open=function(url){ + if(url&&(typeof url==='string')&&(url.indexOf('http://')===0||url.indexOf('https://')===0)){ + if(window.runtime&&window.runtime.BrowserOpenURL){ + window.runtime.BrowserOpenURL(url); + return null; + } + } + return origOpen.apply(window,arguments); +}; })();` func main() { @@ -267,11 +298,11 @@ func main() { Bind: []interface{}{ app, }, + BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 255}, Mac: &mac.Options{ - TitleBar: mac.TitleBarHiddenInset(), // Beautiful borderless titlebar - Appearance: mac.NSAppearanceNameDarkAqua, - WebviewIsTransparent: true, - WindowIsTranslucent: true, + TitleBar: mac.TitleBarHiddenInset(), + WebviewIsTransparent: false, + WindowIsTranslucent: false, }, }) diff --git a/frontend/src/api/auth/index.ts b/frontend/src/api/auth/index.ts index 50003371..17437900 100644 --- a/frontend/src/api/auth/index.ts +++ b/frontend/src/api/auth/index.ts @@ -190,6 +190,21 @@ export async function register(data: RegisterRequest): Promise } } +/** + * Lite 版自动初始化(创建默认用户/租户 + 签发令牌) + */ +export async function autoSetup(): Promise { + try { + const response = await post('/api/v1/auth/auto-setup', {}) + return response as unknown as LoginResponse + } catch (error: any) { + return { + success: false, + message: error.message || 'Auto-setup unavailable' + } + } +} + /** * 获取当前用户信息 */ diff --git a/frontend/src/components/menu.vue b/frontend/src/components/menu.vue index bbdc6830..6985665b 100644 --- a/frontend/src/components/menu.vue +++ b/frontend/src/components/menu.vue @@ -768,6 +768,11 @@ const onDragHandleMouseDown = (e: MouseEvent) => { transition: width 0.25s ease, min-width 0.25s ease; position: relative; + // macOS Wails 桌面:红绿灯位于 HiddenInset 标题栏区域,需让出顶部空间 + html.wails-desktop & { + padding-top: 30px; + } + &--collapsed { min-width: 60px; width: 60px; diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index e46889fb..9dc5564e 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,7 +1,7 @@ import { createRouter, createWebHistory } from 'vue-router' import { listKnowledgeBases } from '@/api/knowledge-base' import { useAuthStore } from '@/stores/auth' -import { validateToken } from '@/api/auth' +import { validateToken, autoSetup } from '@/api/auth' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -105,6 +105,36 @@ const router = createRouter({ ], }); +// 持久化 auto-setup / login 返回的认证信息到 store +function persistLoginResponse(authStore: ReturnType, response: any) { + if (response.user && response.tenant && response.token) { + authStore.setUser({ + id: response.user.id || '', + username: response.user.username || '', + email: response.user.email || '', + avatar: response.user.avatar, + tenant_id: String(response.tenant.id) || '', + can_access_all_tenants: response.user.can_access_all_tenants || false, + created_at: response.user.created_at || new Date().toISOString(), + updated_at: response.user.updated_at || new Date().toISOString() + }) + authStore.setToken(response.token) + if (response.refresh_token) { + authStore.setRefreshToken(response.refresh_token) + } + authStore.setTenant({ + id: String(response.tenant.id) || '', + name: response.tenant.name || '', + api_key: response.tenant.api_key || '', + owner_id: response.user.id || '', + created_at: response.tenant.created_at || new Date().toISOString(), + updated_at: response.tenant.updated_at || new Date().toISOString() + }) + } +} + +let autoSetupAttempted = false + // 路由守卫:检查认证状态和系统初始化状态 router.beforeEach(async (to, from, next) => { const authStore = useAuthStore() @@ -123,26 +153,23 @@ router.beforeEach(async (to, from, next) => { // 检查用户认证状态 if (to.meta.requiresAuth !== false) { if (!authStore.isLoggedIn) { - // 未登录,跳转到登录页面 + // Lite 版:尝试自动初始化(仅一次),成功则跳过登录页 + if (!autoSetupAttempted) { + autoSetupAttempted = true + try { + const response = await autoSetup() + if (response.success) { + persistLoginResponse(authStore, response) + next(to.fullPath) + return + } + } catch { + // auto-setup 不可用(非 lite 版本),走正常登录流程 + } + } next('/login') return } - - // 验证Token有效性 - // try { - // const { valid } = await validateToken() - // if (!valid) { - // // Token无效,清空认证信息并跳转到登录页面 - // authStore.logout() - // next('/login') - // return - // } - // } catch (error) { - // console.error('Token验证失败:', error) - // authStore.logout() - // next('/login') - // return - // } } next() diff --git a/frontend/src/views/auth/Login.vue b/frontend/src/views/auth/Login.vue index a1d17b8f..f62cf75a 100755 --- a/frontend/src/views/auth/Login.vue +++ b/frontend/src/views/auth/Login.vue @@ -377,7 +377,7 @@ import { Autoplay, EffectFade, Pagination } from 'swiper/modules' import 'swiper/css' import 'swiper/css/effect-fade' import 'swiper/css/pagination' -import { login, register, getOIDCAuthorizationURL, getOIDCConfig } from '@/api/auth' +import { login, register, getOIDCAuthorizationURL, getOIDCConfig, autoSetup } from '@/api/auth' import { useAuthStore } from '@/stores/auth' import { useI18n } from 'vue-i18n' @@ -668,13 +668,23 @@ const handleRegister = async () => { } } -// Check if already logged in -onMounted(() => { +// Check if already logged in; for lite edition, attempt transparent auto-setup +onMounted(async () => { if (authStore.isLoggedIn) { router.replace('/platform/knowledge-bases') return } + try { + const response = await autoSetup() + if (response.success) { + await persistLoginResponse(response) + return + } + } catch { + // auto-setup not available (standard edition) — show normal login form + } + loadOIDCConfig() }) diff --git a/internal/handler/auth.go b/internal/handler/auth.go index ff87d95a..bdf03519 100644 --- a/internal/handler/auth.go +++ b/internal/handler/auth.go @@ -1,8 +1,10 @@ package handler import ( + "crypto/rand" "encoding/base64" "encoding/json" + "fmt" "net/http" "os" "strings" @@ -504,6 +506,79 @@ func (h *AuthHandler) ChangePassword(c *gin.Context) { }) } +// AutoSetup godoc +// @Summary 自动初始化(Lite 桌面版) +// @Description Lite 版专用:首次启动时自动创建默认用户和租户并返回令牌,后续启动直接签发令牌,免除手动注册/登录流程 +// @Tags 认证 +// @Accept json +// @Produce json +// @Success 200 {object} types.LoginResponse +// @Failure 403 {object} errors.AppError "非 Lite 版本" +// @Router /auth/auto-setup [post] +func (h *AuthHandler) AutoSetup(c *gin.Context) { + ctx := c.Request.Context() + + if Edition != "lite" { + appErr := errors.NewForbiddenError("auto-setup is only available in lite edition") + c.Error(appErr) + return + } + + const defaultEmail = "admin@weknora.local" + + user, _ := h.userService.GetUserByEmail(ctx, defaultEmail) + if user == nil { + logger.Info(ctx, "Auto-setup: creating default user and tenant for lite edition") + + randomBytes := make([]byte, 24) + if _, err := rand.Read(randomBytes); err != nil { + appErr := errors.NewInternalServerError("auto-setup failed: unable to generate credentials") + c.Error(appErr) + return + } + randomPassword := base64.RawURLEncoding.EncodeToString(randomBytes) + randomUsername := fmt.Sprintf("user_%s", base64.RawURLEncoding.EncodeToString(randomBytes[:6])) + + _, err := h.userService.Register(ctx, &types.RegisterRequest{ + Username: randomUsername, + Email: defaultEmail, + Password: randomPassword, + }) + if err != nil { + logger.Errorf(ctx, "Auto-setup: failed to register default user: %v", err) + appErr := errors.NewInternalServerError("auto-setup failed").WithDetails(err.Error()) + c.Error(appErr) + return + } + user, _ = h.userService.GetUserByEmail(ctx, defaultEmail) + if user == nil { + appErr := errors.NewInternalServerError("auto-setup failed: user not found after registration") + c.Error(appErr) + return + } + } + + accessToken, refreshToken, err := h.userService.GenerateTokens(ctx, user) + if err != nil { + logger.Errorf(ctx, "Auto-setup: failed to generate tokens: %v", err) + appErr := errors.NewInternalServerError("auto-setup failed").WithDetails(err.Error()) + c.Error(appErr) + return + } + + tenant, _ := h.tenantService.GetTenantByID(ctx, user.TenantID) + + logger.Info(ctx, "Auto-setup: completed successfully") + c.JSON(http.StatusOK, &types.LoginResponse{ + Success: true, + Message: "Auto-setup successful", + User: user, + Tenant: tenant, + Token: accessToken, + RefreshToken: refreshToken, + }) +} + // ValidateToken godoc // @Summary 验证令牌 // @Description 验证访问令牌是否有效 diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index 951bd870..e318b637 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -21,6 +21,7 @@ var noAuthAPI = map[string][]string{ "/health": {"GET"}, "/api/v1/auth/register": {"POST"}, "/api/v1/auth/login": {"POST"}, + "/api/v1/auth/auto-setup": {"POST"}, "/api/v1/auth/oidc/config": {"GET"}, "/api/v1/auth/oidc/url": {"GET"}, "/api/v1/auth/oidc/callback": {"GET"}, diff --git a/internal/router/router.go b/internal/router/router.go index 18b5453e..36794a40 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -31,37 +31,37 @@ import ( type RouterParams struct { dig.In - Config *config.Config - UserService interfaces.UserService - KBService interfaces.KnowledgeBaseService - KnowledgeService interfaces.KnowledgeService - ChunkService interfaces.ChunkService - SessionService interfaces.SessionService - MessageService interfaces.MessageService - ModelService interfaces.ModelService - EvaluationService interfaces.EvaluationService - KBHandler *handler.KnowledgeBaseHandler - KnowledgeHandler *handler.KnowledgeHandler - TenantHandler *handler.TenantHandler - TenantService interfaces.TenantService - ChunkHandler *handler.ChunkHandler - SessionHandler *session.Handler - MessageHandler *handler.MessageHandler - ModelHandler *handler.ModelHandler - EvaluationHandler *handler.EvaluationHandler - AuthHandler *handler.AuthHandler - InitializationHandler *handler.InitializationHandler - SystemHandler *handler.SystemHandler - MCPServiceHandler *handler.MCPServiceHandler - WebSearchHandler *handler.WebSearchHandler - WebSearchProviderHandler *handler.WebSearchProviderHandler - FAQHandler *handler.FAQHandler - TagHandler *handler.TagHandler - CustomAgentHandler *handler.CustomAgentHandler - SkillHandler *handler.SkillHandler - OrganizationHandler *handler.OrganizationHandler - IMHandler *handler.IMHandler - DataSourceHandler *handler.DataSourceHandler + Config *config.Config + UserService interfaces.UserService + KBService interfaces.KnowledgeBaseService + KnowledgeService interfaces.KnowledgeService + ChunkService interfaces.ChunkService + SessionService interfaces.SessionService + MessageService interfaces.MessageService + ModelService interfaces.ModelService + EvaluationService interfaces.EvaluationService + KBHandler *handler.KnowledgeBaseHandler + KnowledgeHandler *handler.KnowledgeHandler + TenantHandler *handler.TenantHandler + TenantService interfaces.TenantService + ChunkHandler *handler.ChunkHandler + SessionHandler *session.Handler + MessageHandler *handler.MessageHandler + ModelHandler *handler.ModelHandler + EvaluationHandler *handler.EvaluationHandler + AuthHandler *handler.AuthHandler + InitializationHandler *handler.InitializationHandler + SystemHandler *handler.SystemHandler + MCPServiceHandler *handler.MCPServiceHandler + WebSearchHandler *handler.WebSearchHandler + WebSearchProviderHandler *handler.WebSearchProviderHandler + FAQHandler *handler.FAQHandler + TagHandler *handler.TagHandler + CustomAgentHandler *handler.CustomAgentHandler + SkillHandler *handler.SkillHandler + OrganizationHandler *handler.OrganizationHandler + IMHandler *handler.IMHandler + DataSourceHandler *handler.DataSourceHandler } // NewRouter 创建新的路由 @@ -397,6 +397,7 @@ func RegisterEvaluationRoutes(r *gin.RouterGroup, handler *handler.EvaluationHan func RegisterAuthRoutes(r *gin.RouterGroup, handler *handler.AuthHandler) { r.POST("/auth/register", handler.Register) r.POST("/auth/login", handler.Login) + r.POST("/auth/auto-setup", handler.AutoSetup) r.GET("/auth/oidc/config", handler.GetOIDCConfig) r.GET("/auth/oidc/url", handler.GetOIDCAuthorizationURL) r.GET("/auth/oidc/callback", handler.OIDCRedirectCallback)