diff --git a/cc-haha/src/server/api/heicode-auth.ts b/cc-haha/src/server/api/heicode-auth.ts index fb9316f..dabd70f 100644 --- a/cc-haha/src/server/api/heicode-auth.ts +++ b/cc-haha/src/server/api/heicode-auth.ts @@ -481,9 +481,20 @@ async function handleOAuthCallback(url: URL): Promise { oauthSessions.delete(state) try { + // Pull the user identity fields that the heicode backend's + // /heicode/oauth/authorize redirect now embeds in the query string + // (email, name, role, channel_id, user_id). Without this the TitleBar + // UserPill stays hidden because mcpAuth.email is undefined. + const oauthUser = pickUserFromQuery(url) + const tokenFromQuery = pickTokenFromQuery(url) if (tokenFromQuery) { - await loginAndActivate(session.providerId, tokenFromQuery) + await loginAndActivate( + session.providerId, + tokenFromQuery, + undefined, + oauthUser ? buildMcpAuthFromOAuthQuery(tokenFromQuery, oauthUser) : undefined, + ) return callbackHtml(true, '登录成功,已回到 HeiCode。你现在可以关闭此页面。') } @@ -507,13 +518,67 @@ async function handleOAuthCallback(url: URL): Promise { redirectUri, codeVerifier: session.codeVerifier, }) - await loginAndActivate(session.providerId, exchangedToken) + await loginAndActivate( + session.providerId, + exchangedToken, + undefined, + oauthUser ? buildMcpAuthFromOAuthQuery(exchangedToken, oauthUser) : undefined, + ) return callbackHtml(true, '登录成功,已回到 HeiCode。你现在可以关闭此页面。') } catch (err) { return callbackHtml(false, err instanceof Error ? err.message : String(err)) } } +/** Read user identity fields the heicode backend embeds in the OAuth redirect. */ +function pickUserFromQuery(url: URL): { + email?: string + name?: string + role?: string + channelId?: string + userId?: string +} | null { + const email = url.searchParams.get('email')?.trim() + const name = url.searchParams.get('name')?.trim() + const role = url.searchParams.get('role')?.trim() + const channelId = url.searchParams.get('channel_id')?.trim() + const userId = url.searchParams.get('user_id')?.trim() + if (!email && !name && !role && !channelId && !userId) return null + return { + ...(email && { email }), + ...(name && { name }), + ...(role && { role }), + ...(channelId && { channelId }), + ...(userId && { userId }), + } +} + +/** + * Shape a mcpAuth record from the OAuth-redirect query params. The OAuth flow + * does not deliver a refreshable JWT pair (it only gives the sk- API key), so + * we set placeholder access/refresh tokens. They're only meaningful for the + * credentials flow (Step 1 POST /api/auth/login); this record exists purely to + * surface the user identity on the TitleBar UserPill. + */ +function buildMcpAuthFromOAuthQuery( + apiKey: string, + u: { email?: string; name?: string; role?: string; channelId?: string; userId?: string }, +) { + const now = Date.now() + return { + accessToken: apiKey, + refreshToken: '', + accessExpiresAt: now + 24 * 3600 * 1000, + refreshExpiresAt: now, + managerLoginUrl: '', + ...(u.userId && { userId: u.userId }), + ...(u.channelId && { channelId: u.channelId }), + ...(u.email && { email: u.email }), + ...(u.name && { displayName: u.name }), + ...(u.role && { role: u.role }), + } +} + // ─── Helpers ─────────────────────────────────────────────────── async function loginAndActivate( diff --git a/heicode/controller/heicode_oauth.go b/heicode/controller/heicode_oauth.go index 83a53e7..48e6bd6 100644 --- a/heicode/controller/heicode_oauth.go +++ b/heicode/controller/heicode_oauth.go @@ -85,6 +85,38 @@ func HeicodeOAuthAuthorize(c *gin.Context) { if providerID != "" { q.Set("provider_id", providerID) } + + // Include the authenticated user's identity so the desktop client can + // display it in the title bar / settings without making a separate + // /api/user/self call (that endpoint requires a session cookie which the + // desktop client doesn't have). This is the missing piece that left the + // UserPill empty for OAuth-route logins. + var user model.User + if dbErr := model.DB.First(&user, userID).Error; dbErr == nil { + if email := strings.TrimSpace(user.Email); email != "" { + q.Set("email", email) + } + display := strings.TrimSpace(user.DisplayName) + if display == "" { + display = strings.TrimSpace(user.Username) + } + if display != "" { + q.Set("name", display) + } + switch { + case user.Role >= 100: + q.Set("role", "root") + case user.Role >= 10: + q.Set("role", "admin") + default: + q.Set("role", "user") + } + if g := strings.TrimSpace(user.Group); g != "" { + q.Set("channel_id", g) + } + q.Set("user_id", fmt.Sprintf("%d", userID)) + } + parsed.RawQuery = q.Encode() c.Redirect(http.StatusFound, parsed.String()) }