diff --git a/.env.example b/.env.example
index 7d7d278..ba2bc0d 100644
--- a/.env.example
+++ b/.env.example
@@ -91,6 +91,9 @@ H5_ACCESS_PASSWORD=change-me
# H5_WECHAT_OAUTH_SCOPE=snsapi_userinfo
# 默认回调:{H5_PUBLIC_BASE_URL}/auth/wechat/callback
# H5_WECHAT_OAUTH_CALLBACK_URL=https://goo.tkmind.cn/auth/wechat/callback
+# PC 扫码登录(微信开放平台网站应用,可选;未配置时用手机扫页面链接二维码)
+# H5_WECHAT_OPEN_APP_ID=
+# H5_WECHAT_OPEN_APP_SECRET=
# 本地开发:pnpm dev 会自动启动 server.mjs + vite
# 若只跑前端:pnpm dev:vite(需另开 pnpm dev:server)
diff --git a/db.mjs b/db.mjs
index a70cd20..801b899 100644
--- a/db.mjs
+++ b/db.mjs
@@ -215,6 +215,41 @@ export async function migrateSchema(pool) {
`ALTER TABLE h5_users ADD COLUMN signup_source VARCHAR(32) NULL DEFAULT 'password' AFTER workspace_root`,
);
}
+
+ const oauthStateColumns = [
+ ['intent', "VARCHAR(16) NOT NULL DEFAULT 'login' AFTER utm_campaign"],
+ ['bind_user_id', 'CHAR(36) NULL AFTER intent'],
+ ['auth_mode', "VARCHAR(8) NOT NULL DEFAULT 'mp' AFTER bind_user_id"],
+ ['status', "VARCHAR(16) NOT NULL DEFAULT 'pending' AFTER auth_mode"],
+ ['result_kind', 'VARCHAR(32) NULL AFTER status'],
+ ['result_token', 'VARCHAR(512) NULL AFTER result_kind'],
+ ['result_message', 'VARCHAR(255) NULL AFTER result_token'],
+ ];
+ for (const [column, definition] of oauthStateColumns) {
+ if (!(await columnExists(pool, 'h5_wechat_oauth_states', column))) {
+ await pool.query(
+ `ALTER TABLE h5_wechat_oauth_states ADD COLUMN \`${column}\` ${definition}`,
+ );
+ }
+ }
+
+ await pool.query(`
+ CREATE TABLE IF NOT EXISTS h5_wechat_pending_binds (
+ token VARCHAR(64) PRIMARY KEY,
+ app_id VARCHAR(32) NOT NULL,
+ openid VARCHAR(64) NOT NULL,
+ unionid VARCHAR(64) NULL,
+ nickname VARCHAR(128) NULL,
+ avatar_url VARCHAR(512) NULL,
+ return_to VARCHAR(512) NULL,
+ utm_source VARCHAR(64) NULL,
+ utm_medium VARCHAR(64) NULL,
+ utm_campaign VARCHAR(64) NULL,
+ expires_at BIGINT NOT NULL,
+ created_at BIGINT NOT NULL,
+ KEY idx_wechat_pending_expires (expires_at)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
+ `);
}
export async function initSchema(pool) {
diff --git a/schema.sql b/schema.sql
index 94e6702..4561d11 100644
--- a/schema.sql
+++ b/schema.sql
@@ -668,7 +668,31 @@ CREATE TABLE IF NOT EXISTS h5_wechat_oauth_states (
utm_source VARCHAR(64) NULL,
utm_medium VARCHAR(64) NULL,
utm_campaign VARCHAR(64) NULL,
+ intent VARCHAR(16) NOT NULL DEFAULT 'login',
+ bind_user_id CHAR(36) NULL,
+ auth_mode VARCHAR(8) NOT NULL DEFAULT 'mp',
+ status VARCHAR(16) NOT NULL DEFAULT 'pending',
+ result_kind VARCHAR(32) NULL,
+ result_token VARCHAR(512) NULL,
+ result_message VARCHAR(255) NULL,
expires_at BIGINT NOT NULL,
created_at BIGINT NOT NULL,
- KEY idx_wechat_oauth_state_expires (expires_at)
+ KEY idx_wechat_oauth_state_expires (expires_at),
+ KEY idx_wechat_oauth_state_status (status, expires_at)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+CREATE TABLE IF NOT EXISTS h5_wechat_pending_binds (
+ token VARCHAR(64) PRIMARY KEY,
+ app_id VARCHAR(32) NOT NULL,
+ openid VARCHAR(64) NOT NULL,
+ unionid VARCHAR(64) NULL,
+ nickname VARCHAR(128) NULL,
+ avatar_url VARCHAR(512) NULL,
+ return_to VARCHAR(512) NULL,
+ utm_source VARCHAR(64) NULL,
+ utm_medium VARCHAR(64) NULL,
+ utm_campaign VARCHAR(64) NULL,
+ expires_at BIGINT NOT NULL,
+ created_at BIGINT NOT NULL,
+ KEY idx_wechat_pending_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/server.mjs b/server.mjs
index 11e181d..b85d079 100644
--- a/server.mjs
+++ b/server.mjs
@@ -15,6 +15,7 @@ import { createTkmindProxy } from './tkmind-proxy.mjs';
import {
clearUserSessionCookie,
createUserAuth,
+ resolveCookieDomainForRequest,
USER_COOKIE,
userLoginCookies,
userSessionCookie,
@@ -340,6 +341,20 @@ function userToken(req) {
return parseCookies(req.get('cookie'))[USER_COOKIE];
}
+function setUserLoginCookies(res, req, token) {
+ res.set(
+ 'Set-Cookie',
+ userLoginCookies(token, isSecureRequest(req), resolveCookieDomainForRequest(req)),
+ );
+}
+
+function clearUserLoginCookies(res, req) {
+ res.set(
+ 'Set-Cookie',
+ clearUserSessionCookie(isSecureRequest(req), resolveCookieDomainForRequest(req)),
+ );
+}
+
async function attachUserSession(req, _res, next) {
if (!userAuth) return next();
const token = userToken(req);
@@ -394,7 +409,7 @@ app.post('/auth/login', jsonBody, async (req, res) => {
}
return res.status(401).json({ message: result.message });
}
- res.set('Set-Cookie', userLoginCookies(result.token, secure));
+ setUserLoginCookies(res, req, result.token);
return res.json({ authenticated: true, user: result.user, mode: 'user' });
}
@@ -460,18 +475,145 @@ app.post('/auth/reset-password', jsonBody, async (req, res) => {
app.get('/auth/wechat/config', async (req, res) => {
await userAuthReady;
if (!wechatOAuthService?.enabled) {
- return res.json({ enabled: false, inWechat: isWechatUserAgent(req.get('user-agent') || '') });
+ return res.json({
+ enabled: false,
+ inWechat: isWechatUserAgent(req.get('user-agent') || ''),
+ scanEnabled: false,
+ });
}
return res.json(wechatOAuthService.publicConfig(req));
});
+app.get('/auth/wechat/status', async (req, res) => {
+ await userAuthReady;
+ if (!userAuth || !wechatOAuthService?.enabled) {
+ return res.json({ enabled: false, bound: false });
+ }
+ const me = await userAuth.getMe(userToken(req));
+ if (!me) return res.status(401).json({ message: '未登录' });
+ const config = loadWechatOAuthConfig();
+ const status = await userAuth.getWechatBindingStatus(me.id, config.appId);
+ return res.json({ enabled: true, ...status });
+});
+
+app.get('/auth/wechat/pending/:token', async (req, res) => {
+ await userAuthReady;
+ if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
+ const pending = await userAuth.getWechatPendingBind(req.params.token);
+ if (!pending) return res.status(404).json({ message: '绑定会话已过期,请重新微信登录' });
+ return res.json({
+ nickname: pending.nickname,
+ avatarUrl: pending.avatar_url,
+ returnTo: pending.return_to || '/',
+ });
+});
+
+app.post('/auth/wechat/register', jsonBody, async (req, res) => {
+ await userAuthReady;
+ const secure = isSecureRequest(req);
+ if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
+ const pendingToken = typeof req.body?.pendingToken === 'string' ? req.body.pendingToken : '';
+ if (!pendingToken) return res.status(400).json({ message: '缺少绑定会话' });
+ const result = await userAuth.completeWechatRegister({ pendingToken });
+ if (!result.ok) return res.status(400).json({ message: result.message });
+ if (result.isNewUser && plazaSeo) {
+ void plazaSeo
+ .recordAttribution(
+ {
+ event_type: 'signup',
+ utm_source: result.utmSource || 'wechat',
+ utm_medium: result.utmMedium,
+ utm_campaign: result.utmCampaign,
+ user_id: result.user?.id ?? null,
+ },
+ plazaClientIp(req),
+ )
+ .catch(() => {});
+ }
+ setUserLoginCookies(res, req, result.token);
+ return res.json({
+ authenticated: true,
+ user: result.user,
+ returnTo: result.returnTo || '/',
+ });
+});
+
+app.post('/auth/wechat/bind', jsonBody, async (req, res) => {
+ await userAuthReady;
+ const secure = isSecureRequest(req);
+ if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
+ const pendingToken = typeof req.body?.pendingToken === 'string' ? req.body.pendingToken : '';
+ const username = typeof req.body?.username === 'string' ? req.body.username : '';
+ const password = typeof req.body?.password === 'string' ? req.body.password : '';
+ if (!pendingToken) return res.status(400).json({ message: '缺少绑定会话' });
+ if (!username || !password) {
+ return res.status(400).json({ message: '用户名和密码不能为空' });
+ }
+ const result = await userAuth.completeWechatBindAccount({
+ pendingToken,
+ username,
+ password,
+ ip: req.ip,
+ });
+ if (!result.ok) {
+ const status = result.retryAfterMs > 0 ? 429 : 401;
+ if (result.retryAfterMs > 0) {
+ res.set('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));
+ }
+ return res.status(status).json({ message: result.message });
+ }
+ setUserLoginCookies(res, req, result.token);
+ return res.json({
+ authenticated: true,
+ user: result.user,
+ bound: true,
+ returnTo: result.returnTo || '/',
+ });
+});
+
+app.post('/auth/wechat/scan/start', async (req, res) => {
+ await userAuthReady;
+ if (!wechatOAuthService?.enabled) {
+ return res.status(503).json({ message: '微信登录未启用' });
+ }
+ try {
+ const payload = await wechatOAuthService.startScanLogin(req);
+ return res.json(payload);
+ } catch (err) {
+ return res.status(503).json({
+ message: err instanceof Error ? err.message : '微信扫码登录不可用',
+ });
+ }
+});
+
+app.get('/auth/wechat/scan/poll', async (req, res) => {
+ await userAuthReady;
+ if (!wechatOAuthService?.enabled) {
+ return res.status(503).json({ message: '微信登录未启用' });
+ }
+ const state = typeof req.query?.state === 'string' ? req.query.state : '';
+ if (!state) return res.status(400).json({ message: '缺少扫码状态' });
+ const result = await wechatOAuthService.pollScanLogin(state);
+ if (result.status === 'complete' && result.token) {
+ setUserLoginCookies(res, req, result.token);
+ }
+ return res.json(result);
+});
+
app.get('/auth/wechat/authorize', async (req, res) => {
await userAuthReady;
if (!wechatOAuthService?.enabled) {
return res.status(503).json({ message: '微信登录未启用' });
}
try {
- const redirectUrl = await wechatOAuthService.buildAuthorizeRedirect(req);
+ let bindUserId = null;
+ const intent = typeof req.query?.intent === 'string' ? req.query.intent.trim().toLowerCase() : 'login';
+ if (intent === 'bind' && userAuth) {
+ const me = await userAuth.getMe(userToken(req));
+ if (!me) return res.status(401).json({ message: '请先登录后再绑定微信' });
+ bindUserId = me.id;
+ }
+ const redirectUrl = await wechatOAuthService.buildAuthorizeRedirect(req, { bindUserId });
return res.redirect(302, redirectUrl);
} catch (err) {
console.error('WeChat authorize failed:', err);
@@ -491,6 +633,23 @@ app.get('/auth/wechat/callback', async (req, res) => {
state: typeof req.query?.state === 'string' ? req.query.state : '',
ip: req.ip,
});
+
+ if (result.action === 'binding_gate') {
+ const params = new URLSearchParams();
+ params.set('wechat_pending', result.pendingToken);
+ if (result.returnTo && result.returnTo !== '/') {
+ params.set('return_to', result.returnTo);
+ }
+ return res.redirect(302, `/?${params.toString()}`);
+ }
+
+ if (result.action === 'poll_error') {
+ return res.redirect(
+ 302,
+ `/?wechat_error=${encodeURIComponent(result.message || '微信登录失败')}`,
+ );
+ }
+
if (result.isNewUser && plazaSeo) {
void plazaSeo
.recordAttribution(
@@ -505,7 +664,12 @@ app.get('/auth/wechat/callback', async (req, res) => {
)
.catch(() => {});
}
- res.set('Set-Cookie', userLoginCookies(result.token, secure));
+
+ if (result.authMode === 'open') {
+ return res.send(`
微信登录扫码登录成功,请返回电脑继续操作。
`);
+ }
+
+ setUserLoginCookies(res, req, result.token);
return res.redirect(302, result.returnTo || '/');
} catch (err) {
console.error('WeChat callback failed:', err);
@@ -537,7 +701,7 @@ app.post('/auth/logout', async (req, res) => {
const secure = isSecureRequest(req);
if (userAuth) {
await userAuth.revoke(userToken(req));
- res.set('Set-Cookie', clearUserSessionCookie(secure));
+ clearUserLoginCookies(res, req);
}
if (legacyAuth) {
legacyAuth.revoke(legacySessionToken(req));
diff --git a/src/App.tsx b/src/App.tsx
index 9aadc65..687d6c8 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -183,12 +183,10 @@ export function App() {
if (returnTo) {
try {
const url = new URL(returnTo, window.location.origin);
- if (url.origin !== window.location.origin) {
+ if (url.origin === window.location.origin) {
window.location.href = url.toString();
return;
}
- window.location.href = url.toString();
- return;
} catch {
// ignore invalid URLs
}
diff --git a/src/api/client.ts b/src/api/client.ts
index b2025f2..613d715 100644
--- a/src/api/client.ts
+++ b/src/api/client.ts
@@ -222,6 +222,20 @@ export async function rememberProjectContext(
export type WechatAuthConfig = {
enabled: boolean;
inWechat?: boolean;
+ scanEnabled?: boolean;
+};
+
+export type WechatPendingProfile = {
+ nickname: string | null;
+ avatarUrl: string | null;
+ returnTo: string;
+};
+
+export type WechatBindingStatus = {
+ enabled: boolean;
+ bound: boolean;
+ nickname?: string | null;
+ avatarUrl?: string | null;
};
export async function getWechatAuthConfig(): Promise {
@@ -1133,6 +1147,141 @@ export async function resetPassword(
}
}
+export async function getWechatPendingProfile(token: string): Promise {
+ let response: Response;
+ try {
+ response = await fetch(`/auth/wechat/pending/${encodeURIComponent(token)}`);
+ } catch (err) {
+ throw new ApiError(0, formatNetworkError(err));
+ }
+ const body = (await response.json().catch(() => null)) as WechatPendingProfile & {
+ message?: string;
+ };
+ if (!response.ok) {
+ throw new ApiError(response.status, body?.message ?? '绑定会话无效');
+ }
+ return body;
+}
+
+export async function completeWechatRegister(pendingToken: string): Promise<{
+ user: PortalUser;
+ returnTo: string;
+}> {
+ let response: Response;
+ try {
+ response = await fetch('/auth/wechat/register', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ pendingToken }),
+ });
+ } catch (err) {
+ throw new ApiError(0, formatNetworkError(err));
+ }
+ const body = (await response.json().catch(() => null)) as {
+ message?: string;
+ user?: PortalUser;
+ returnTo?: string;
+ };
+ if (!response.ok || !body?.user) {
+ throw new ApiError(response.status, body?.message ?? '微信注册失败');
+ }
+ return { user: body.user, returnTo: body.returnTo ?? '/' };
+}
+
+export async function completeWechatBind(
+ pendingToken: string,
+ username: string,
+ password: string,
+): Promise<{ user: PortalUser; returnTo: string }> {
+ let response: Response;
+ try {
+ response = await fetch('/auth/wechat/bind', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ pendingToken, username, password }),
+ });
+ } catch (err) {
+ throw new ApiError(0, formatNetworkError(err));
+ }
+ const body = (await response.json().catch(() => null)) as {
+ message?: string;
+ user?: PortalUser;
+ returnTo?: string;
+ };
+ if (!response.ok || !body?.user) {
+ throw new ApiError(response.status, body?.message ?? '绑定失败');
+ }
+ return { user: body.user, returnTo: body.returnTo ?? '/' };
+}
+
+export async function getWechatBindingStatus(): Promise {
+ try {
+ const response = await fetch('/auth/wechat/status');
+ if (!response.ok) return { enabled: false, bound: false };
+ return (await response.json()) as WechatBindingStatus;
+ } catch {
+ return { enabled: false, bound: false };
+ }
+}
+
+export async function startWechatScanLogin(returnTo?: string): Promise<{
+ state: string;
+ qrUrl: string;
+ expiresInMs: number;
+}> {
+ const params = new URLSearchParams();
+ if (returnTo) params.set('return_to', returnTo);
+ const query = params.toString();
+ let response: Response;
+ try {
+ response = await fetch(`/auth/wechat/scan/start${query ? `?${query}` : ''}`, {
+ method: 'POST',
+ });
+ } catch (err) {
+ throw new ApiError(0, formatNetworkError(err));
+ }
+ const body = (await response.json().catch(() => null)) as {
+ message?: string;
+ state?: string;
+ qrUrl?: string;
+ expiresInMs?: number;
+ };
+ if (!response.ok || !body?.state || !body?.qrUrl) {
+ throw new ApiError(response.status, body?.message ?? '无法启动扫码登录');
+ }
+ return {
+ state: body.state,
+ qrUrl: body.qrUrl,
+ expiresInMs: body.expiresInMs ?? 600000,
+ };
+}
+
+export async function pollWechatScanLogin(state: string): Promise<{
+ status: 'pending' | 'complete' | 'binding_gate' | 'error' | 'expired';
+ pendingToken?: string;
+ message?: string;
+}> {
+ let response: Response;
+ try {
+ response = await fetch(`/auth/wechat/scan/poll?state=${encodeURIComponent(state)}`);
+ } catch (err) {
+ throw new ApiError(0, formatNetworkError(err));
+ }
+ const body = (await response.json().catch(() => null)) as {
+ status?: 'pending' | 'complete' | 'binding_gate' | 'error' | 'expired';
+ pendingToken?: string;
+ message?: string;
+ };
+ if (!response.ok) {
+ throw new ApiError(response.status, body?.message ?? '扫码状态查询失败');
+ }
+ return {
+ status: body?.status ?? 'error',
+ pendingToken: body?.pendingToken,
+ message: body?.message,
+ };
+}
+
export async function register(
username: string,
password: string,
diff --git a/src/components/AuthView.tsx b/src/components/AuthView.tsx
index aadb0bb..0e49636 100644
--- a/src/components/AuthView.tsx
+++ b/src/components/AuthView.tsx
@@ -1,9 +1,26 @@
import { useEffect, useState } from 'react';
-import { getMe, getWechatAuthConfig, login, loginLegacy, register, resetPassword } from '../api/client';
+import {
+ getMe,
+ getWechatAuthConfig,
+ login,
+ loginLegacy,
+ register,
+ resetPassword,
+} from '../api/client';
import type { CapabilityMap, PortalUser } from '../types';
-import { buildWechatAuthorizeUrl, isWechatContext, readWechatAuthError } from '../utils/wechat';
+import {
+ buildWechatAuthorizeUrl,
+ isMobileExternalBrowser,
+ isWechatContext,
+ readWechatAuthError,
+ readWechatPendingToken,
+} from '../utils/wechat';
import { TKMindAvatar } from './TKMindAvatar';
import { resolvePlazaHomeUrl } from '../utils/publicUrl';
+import { WechatBindGate } from './WechatBindGate';
+import { WechatDesktopLogin } from './WechatDesktopLogin';
+import { WechatIcon } from './WechatIcon';
+import { WechatOpenGuide } from './WechatOpenGuide';
type Mode = 'login' | 'register' | 'reset';
@@ -34,9 +51,16 @@ export function AuthView({
const [success, setSuccess] = useState(null);
const [submitting, setSubmitting] = useState(false);
const [wechatEnabled, setWechatEnabled] = useState(false);
+ const [scanEnabled, setScanEnabled] = useState(false);
const [wechatContext, setWechatContext] = useState(() =>
isWechatContext({ search: window.location.search }),
);
+ const [mobileExternal, setMobileExternal] = useState(() =>
+ isMobileExternalBrowser({ search: window.location.search }),
+ );
+ const [showPasswordLogin, setShowPasswordLogin] = useState(false);
+ const [showScanLogin, setShowScanLogin] = useState(false);
+ const [pendingToken, setPendingToken] = useState(() => readWechatPendingToken());
const searchParams = new URLSearchParams(window.location.search);
const fromPlaza = (searchParams.get('utm_source') ?? '').toLowerCase() === 'plaza';
const returnTo = searchParams.get('return_to');
@@ -52,22 +76,54 @@ export function AuthView({
if (legacyMode) return;
void getWechatAuthConfig().then((config) => {
setWechatEnabled(config.enabled);
+ setScanEnabled(Boolean(config.scanEnabled));
setWechatContext((prev) =>
isWechatContext({
search: window.location.search,
serverInWechat: config.inWechat,
}) || prev,
);
+ setMobileExternal(
+ isMobileExternalBrowser({
+ search: window.location.search,
+ serverInWechat: config.inWechat,
+ }),
+ );
});
}, [legacyMode]);
- const showWechatLogin = !legacyMode && mode === 'login' && wechatContext && wechatEnabled;
+ const isLogin = mode === 'login' && !legacyMode;
+ const showWechatOneClick = isLogin && wechatContext && wechatEnabled;
+ const showMobileWechatGuide = isLogin && mobileExternal && wechatEnabled;
+ const showDesktopWechat = isLogin && wechatEnabled && !wechatContext && !mobileExternal;
+ const isDesktopScanMode = showDesktopWechat && showScanLogin;
+ const showAccountForm =
+ !isDesktopScanMode &&
+ (legacyMode ||
+ mode !== 'login' ||
+ mobileExternal ||
+ showPasswordLogin ||
+ !showWechatOneClick ||
+ showDesktopWechat);
+
+ const switchToPasswordLogin = () => {
+ setShowScanLogin(false);
+ setError(null);
+ setSuccess(null);
+ };
+
+ const switchToScanLogin = () => {
+ setShowScanLogin(true);
+ setError(null);
+ setSuccess(null);
+ };
const switchMode = (next: Mode) => {
setMode(next);
setError(null);
setSuccess(null);
setConfirmPassword('');
+ setShowScanLogin(false);
};
const handleWechatLogin = () => {
@@ -77,6 +133,7 @@ export function AuthView({
utmSource: params.get('utm_source') ?? params.get('from') ?? 'wechat',
utmMedium: params.get('utm_medium') ?? undefined,
utmCampaign: params.get('utm_campaign') ?? undefined,
+ intent: 'login',
});
};
@@ -119,6 +176,7 @@ export function AuthView({
);
const user = await login(username, password);
const me = await getMe().catch(() => null);
+ sessionStorage.setItem('tkmind_suggest_wechat_bind', '1');
onAuth(user, me?.capabilities, me?.grantedSkills);
} catch (err) {
setError(err instanceof Error ? err.message : '注册失败');
@@ -149,6 +207,16 @@ export function AuthView({
}
};
+ if (pendingToken) {
+ return (
+
+ );
+ }
+
const meta = legacyMode
? { title: 'TKMind', desc: '内部访问入口' }
: fromPlaza
@@ -201,6 +269,9 @@ export function AuthView({
!password.trim() ||
!confirmPassword.trim());
+ const showAltLogin =
+ isLogin && wechatEnabled && (showMobileWechatGuide || (showDesktopWechat && !showScanLogin));
+
return (
@@ -236,7 +307,24 @@ export function AuthView({
{meta.desc}
- {showWechatLogin && (
+ {isDesktopScanMode && (
+ <>
+
setPendingToken(token)}
+ onComplete={() => window.location.reload()}
+ onError={(message) => setError(message)}
+ />
+ {error && {error}
}
+ {success && {success}
}
+
+ >
+ )}
+
+ {showWechatOneClick && (
<>
-
- 或
-
+ {!showPasswordLogin ? (
+
+ ) : (
+
+ 或
+
+ )}
>
)}
-
+ )}
diff --git a/src/components/ChatView.tsx b/src/components/ChatView.tsx
index 3ce134b..949f6a9 100644
--- a/src/components/ChatView.tsx
+++ b/src/components/ChatView.tsx
@@ -7,7 +7,8 @@ import { openAvatarPicker } from '../utils/userAvatar';
import { HistorySidebar } from './HistorySidebar';
import { TKMindAvatar } from './TKMindAvatar';
import { ChatPanel } from './ChatPanel';
-import { BalanceRing } from './BalanceRing';
+import { WechatBindPrompt } from './WechatBindPrompt';
+import { WechatAccountButton } from './WechatAccountButton';
import type { MindSpaceSaveCategory } from '../types';
export function ChatView({
@@ -144,6 +145,7 @@ export function ChatView({
+ {user && }
{onLogout && (