优化微信扫码登录流程,扫码直接登录,无需再次输入验证码 - #2508
Conversation
…d size validation
feat: EditTokenModal 中针对用户创建的 token 默认无限额度
feat: add environment variable switch for critical rate limit
…80p-image fix: trim suffix p for jimeng image model
…space fix: tag splitting by whitespace
…odisable fix(channel): 当没有可用密钥时返回错误而不是第一个密钥
… new sections for partners, acknowledgments, and deployment instructions
- 更新中文README.md中的语言链接 - 完全重写英文README.en.md,包含所有详细功能说明 - 完全重写法文README.fr.md,确保内容一致性 - 完全重写日文README.ja.md,提供完整的项目说明 所有语言版本现在具有: - 相同的结构和格式 - 一致的语言导航 - 完整的功能特性和部署指南 - 统一的环境变量配置说明
…ence 修复viduq2不支持参考生视频的问题
…annel feat: replicate channel flux model
fix GetChannelKey AdminAuth -> RootAuth
fix GetChannelKey AdminAuth -> RootAuth
…future adjustments
…in GetAudioDuration" This reverts commit e293be0.
…itelist-cidr feat(auth): enhance IP restriction handling with CIDR support
… meta when token count is disabled Clamp request body size (including post-decompression) to avoid memory exhaustion caused by huge payloads/zip bombs, especially with large-context Claude requests. Add a configurable `MAX_REQUEST_BODY_MB` (default `32`) and document it. - Enforce max request body size after gzip/br decompression via `http.MaxBytesReader` - Add a secondary size guard in `common.GetRequestBody` and cache-safe handling - Return **413 Request Entity Too Large** on oversized bodies in relay entry - Avoid building large `TokenCountMeta.CombineText` when both token counting and sensitive check are disabled (use lightweight meta for pricing) - Update READMEs (CN/EN/FR/JA) with `MAX_REQUEST_BODY_MB` - Fix a handful of vet/formatting issues encountered during the change - `go test ./...` passes
Tighten oversized request handling across relay paths and make error matching reliable. - Align `MAX_REQUEST_BODY_MB` fallback to `32` in request body reader and decompression middleware - Stop ignoring `GetRequestBody` errors in relay retry paths; return consistent **413** on oversized bodies (400 for other read errors) - Add `Unwrap()` to `types.NewAPIError` so `errors.Is/As` can match wrapped underlying errors - `go test ./...` passes
…-response-id fix: 模型设置增加针对Vertex渠道过滤content[].part[].functionResponse.id的选项,默认启用
- Replace legacy `docs.newapi.pro` paths with the new `/{lang}/docs/...` structure across all README translations
- Point key sections (installation, env vars, API, support, features) to their new locations
- Ensure language-specific links use the correct locale prefix (zh/en/ja) and keep FR aligned with English routes
Keep new-site links (/{lang}/docs/...) where matching pages exist in the current docs repo
Revert links that have no equivalent in the new docs to the legacy paths on doc.newapi.pro:
Google Gemini Chat
Midjourney-Proxy image docs
Suno music docs
Apply the same rule consistently across all README translations (zh/en/ja/fr)
…te-doc-links-new-routing 🔗 docs(readme): update documentation links to new site routing
WalkthroughThis PR adds a new WeChat direct login feature using QR codes. Backend changes include a new feature flag constant, two proxy API endpoints for QR code generation and login status polling, option management support, and API routing. Frontend changes introduce dual-mode authentication flows for both login and account binding, with QR code display, status polling logic, and UI rendering conditionally based on the feature flag. Localization support is added across six languages. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Client as Browser Client
participant API as Backend API
participant WSvr as WeChat Server
rect rgb(200, 230, 255)
Note over User,WSvr: Direct WeChat QR Code Login Flow
User->>Client: Click WeChat Login
Client->>API: POST /api/wechat/create_login_qrcode
API->>WSvr: POST create QR code (wechat-server proxy)
WSvr-->>API: {login_token, qrcode_url}
API-->>Client: {login_token, qrcode_url}
Client->>Client: Display QR code, start polling
end
rect rgb(240, 200, 230)
Note over User,WSvr: Polling Phase (repeats every 2s, max 10 min)
User->>User: Scan QR on WeChat client
Client->>API: GET /api/wechat/login_status?login_token=...
API->>WSvr: GET fetch login status (wechat-server proxy)
alt User authorized on WeChat
WSvr-->>API: {auth_code}
API-->>Client: {auth_code}
Client->>Client: Stop polling
Client->>API: POST /login with auth_code
API-->>Client: {session_token}
Client->>Client: Login successful, navigate home
else Poll timeout (10 min exceeded)
Client->>Client: Show error, stop polling
else Pending (no user action yet)
WSvr-->>API: {status: 'pending'}
API-->>Client: {status: 'pending'}
Client->>Client: Continue polling
end
end
sequenceDiagram
autonumber
actor User
participant Client as Browser Client
participant API as Backend API
participant WSvr as WeChat Server
rect rgb(200, 230, 255)
Note over User,WSvr: Direct WeChat Binding Flow
User->>Client: Open WeChat Bind Modal
Client->>API: POST /api/wechat/create_login_qrcode
API->>WSvr: POST create QR code
WSvr-->>API: {login_token, qrcode_url}
API-->>Client: {login_token, qrcode_url}
Client->>Client: Display QR code, start polling
end
rect rgb(240, 200, 230)
Note over User,WSvr: Polling & Binding
User->>User: Scan QR on WeChat
loop Poll Every 2s
Client->>API: GET /api/wechat/login_status?login_token=...
API->>WSvr: GET fetch status
WSvr-->>API: status response
API-->>Client: status response
end
alt User authorized
Client->>Client: Stop polling
Client->>API: POST /bind with auth_code
API-->>Client: Bind success
Client->>Client: Trigger onBindSuccess callback
Client->>API: GET /getUserData (refresh)
API-->>Client: Updated user info
Client->>Client: Update local state, close modal
else Expired or Error
Client->>Client: Show error message, stop polling
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/components/settings/PersonalSetting.jsx (1)
302-319: RedundantgetUserData()calls after WeChat bind.The
bindWeChatfunction callsgetUserData()at line 311, and the newhandleWeChatBindSuccesscallback (passed toWeChatBindModalat line 513) also callsgetUserData()at line 318. This results in two identical API calls when WeChat binding succeeds.Recommendation: Remove the
getUserData()call frombindWeChat(line 311) since the callback pattern now handles the refresh. TheWeChatBindModalshould triggeronBindSuccessafter a successful bind, making the call inbindWeChatunnecessary.🔎 Suggested fix
const bindWeChat = async () => { if (inputs.wechat_verification_code === '') return; const res = await API.get( `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`, ); const { success, message } = res.data; if (success) { showSuccess(t('微信账户绑定成功!')); setShowWeChatBindModal(false); - await getUserData(); } else { showError(message); } };
🧹 Nitpick comments (4)
web/src/i18n/locales/vi.json (1)
559-559: Vietnamese translation is correct; optional nuance improvementThe new string is accurate and consistent with other “启用…” → “Bật …” settings.
If you want slightly more natural Vietnamese, you could consider:Optional wording tweak
- "启用微信扫码直接登录": "Bật đăng nhập trực tiếp bằng mã QR WeChat", + "启用微信扫码直接登录": "Bật đăng nhập trực tiếp bằng quét mã QR WeChat",web/src/i18n/locales/ja.json (1)
559-559: Japanese label is correct and consistent with other “启用…” toggles
"WeChatスキャンコード直接ログインを有効にする"accurately reflects “启用微信扫码直接登录” and matches the “…を有効にする” pattern used elsewhere. If you want slightly more natural phrasing later, something likeWeChatのQRコードによる直接ログインを有効にするwould also work, but it’s not required.web/src/i18n/locales/en.json (1)
598-598: English translation is accurate; optional micro‑polish“Enable WeChat QR Code Direct Login” is clear and consistent with other “Enable …” toggles. If you want slightly more natural English, you could use e.g. “Enable direct WeChat QR code login” later, but current wording is perfectly acceptable.
web/src/components/settings/personal/modals/WeChatBindModal.jsx (1)
84-107: Consider extracting shared QR code polling logic.The QR code generation and polling logic in
WeChatBindModal.jsxandLoginForm.jsxare nearly identical. Consider extracting this into a custom hook (e.g.,useWeChatQRCodeLogin) to reduce duplication and ensure consistent behavior across both components.Also applies to: 109-143
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
common/constants.gocontroller/misc.gocontroller/wechat.gomodel/option.gorouter/api-router.goweb/src/components/auth/LoginForm.jsxweb/src/components/settings/PersonalSetting.jsxweb/src/components/settings/SystemSetting.jsxweb/src/components/settings/personal/modals/WeChatBindModal.jsxweb/src/i18n/locales/en.jsonweb/src/i18n/locales/fr.jsonweb/src/i18n/locales/ja.jsonweb/src/i18n/locales/ru.jsonweb/src/i18n/locales/vi.jsonweb/src/i18n/locales/zh.json
🧰 Additional context used
🧬 Code graph analysis (6)
controller/misc.go (1)
common/constants.go (1)
WeChatDirectLoginEnabled(92-92)
model/option.go (1)
common/constants.go (2)
OptionMap(37-37)WeChatDirectLoginEnabled(92-92)
web/src/components/settings/personal/modals/WeChatBindModal.jsx (2)
web/src/components/auth/LoginForm.jsx (3)
status(110-113)inputs(62-66)inputs(67-67)web/src/components/settings/PersonalSetting.jsx (4)
status(61-61)showWeChatBindModal(63-63)inputs(52-60)bindWeChat(302-315)
controller/wechat.go (2)
common/constants.go (4)
WeChatAuthEnabled(48-48)WeChatDirectLoginEnabled(92-92)WeChatServerAddress(89-89)WeChatServerToken(90-90)types/set.go (1)
Set(3-5)
web/src/components/auth/LoginForm.jsx (2)
web/src/components/auth/RegisterForm.jsx (4)
onWeChatLoginClicked(145-149)status(103-106)turnstileEnabled(70-70)turnstileToken(72-72)web/src/components/settings/PersonalSetting.jsx (3)
status(61-61)turnstileEnabled(66-66)turnstileToken(68-68)
router/api-router.go (2)
middleware/rate-limit.go (1)
CriticalRateLimit(104-109)controller/wechat.go (2)
CreateLoginQRCode(172-217)GetLoginStatus(220-273)
🔇 Additional comments (14)
model/option.go (1)
103-103: LGTM! Consistent option handling for WeChat direct login.The implementation correctly follows the established pattern for boolean configuration options:
- Initialized in
InitOptionMapusingstrconv.FormatBool- Updated in
updateOptionMapusing thevalue == "true"check- Consistent with other similar options like
WeChatAuthEnabledAlso applies to: 382-383
controller/misc.go (1)
67-67: LGTM! Feature flag correctly exposed in status endpoint.The new
wechat_direct_login_enabledfield is properly added to the status response, following the same pattern as other authentication flags (e.g.,wechat_login,telegram_oauth).router/api-router.go (1)
39-41: LGTM! WeChat direct login routes properly configured.The two new API endpoints are correctly set up:
- Appropriate rate limiting with
CriticalRateLimitmiddleware- Logically grouped with existing WeChat OAuth routes
- REST conventions followed (POST for creation, GET for status check)
common/constants.go (1)
92-92: LGTM! Safe default for WeChat direct login feature flag.The constant is properly defined with:
- Sensible default value (
false) requiring explicit opt-in- Clear documentation comment explaining the toggle behavior
- Appropriate placement alongside related WeChat configuration
web/src/i18n/locales/fr.json (1)
601-601: LGTM! French localization added for WeChat direct login feature.The translation for "启用微信扫码直接登录" → "Activer la connexion directe par code QR WeChat" is properly added and maintains consistency with the file's formatting.
web/src/i18n/locales/ru.json (1)
605-605: Russian translation matches semantics and existing style“Включить прямой вход через QR-код WeChat” correctly reflects the source and is consistent with other “启用…” settings strings in this locale.
web/src/i18n/locales/zh.json (1)
593-593: Chinese entry matches key and existing toggle styleThe new key/value
"启用微信扫码直接登录"is consistent with other “启用…” toggles and correctly describes the feature. No changes needed.web/src/components/settings/SystemSetting.jsx (2)
79-79: LGTM!The
WeChatDirectLoginEnabledoption is correctly added to the inputs state following the same pattern as other boolean options.
1080-1088: LGTM!The checkbox is correctly wired to the
WeChatDirectLoginEnabledoption with properhandleCheckboxChangeintegration. The label appropriately describes the feature ("启用微信扫码直接登录").controller/wechat.go (1)
171-217: LGTM!The
CreateLoginQRCodehandler correctly validates both feature flags, sets appropriate headers, uses a reasonable timeout, and handles errors properly.web/src/components/settings/personal/modals/WeChatBindModal.jsx (2)
109-143: LGTM!The polling logic is well-implemented with proper cleanup on all exit paths (success, expired, max polls reached). The 2-second interval and 10-minute maximum duration are reasonable for this use case.
145-165: LGTM!The
bindWithAuthCodefunction correctly handles success/error cases, invokesonBindSuccesscallback for parent notification, and uses a slight delay before closing the modal to allow the success message to be visible.web/src/components/auth/LoginForm.jsx (2)
77-81: LGTM!The
directLoginDatastate structure is clean and appropriate for tracking the QR code login flow.
928-963: LGTM!The
renderDirectLoginfunction correctly displays the QR code, polling status, and refresh button. The loading state handling is appropriate.
| loginToken := c.Query("login_token") | ||
| if loginToken == "" { | ||
| c.JSON(http.StatusBadRequest, gin.H{ | ||
| "success": false, | ||
| "message": "缺少login_token参数", | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| // 代理请求到wechat-server | ||
| req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/login_status?login_token=%s", common.WeChatServerAddress, loginToken), nil) |
There was a problem hiding this comment.
URL-encode the loginToken query parameter to prevent injection.
The loginToken is user-supplied input directly interpolated into the URL. If it contains special characters (e.g., &, #, ?), it could break the URL structure or potentially be exploited.
🔎 Proposed fix
+import "net/url"
+
// 代理查询登录状态请求
func GetLoginStatus(c *gin.Context) {
if !common.WeChatAuthEnabled || !common.WeChatDirectLoginEnabled {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员未开启微信扫码直接登录功能",
})
return
}
loginToken := c.Query("login_token")
if loginToken == "" {
c.JSON(http.StatusBadRequest, gin.H{
"success": false,
"message": "缺少login_token参数",
})
return
}
// 代理请求到wechat-server
- req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/login_status?login_token=%s", common.WeChatServerAddress, loginToken), nil)
+ req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/login_status?login_token=%s", common.WeChatServerAddress, url.QueryEscape(loginToken)), nil)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| loginToken := c.Query("login_token") | |
| if loginToken == "" { | |
| c.JSON(http.StatusBadRequest, gin.H{ | |
| "success": false, | |
| "message": "缺少login_token参数", | |
| }) | |
| return | |
| } | |
| // 代理请求到wechat-server | |
| req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/login_status?login_token=%s", common.WeChatServerAddress, loginToken), nil) | |
| import "net/url" | |
| loginToken := c.Query("login_token") | |
| if loginToken == "" { | |
| c.JSON(http.StatusBadRequest, gin.H{ | |
| "success": false, | |
| "message": "缺少login_token参数", | |
| }) | |
| return | |
| } | |
| // 代理请求到wechat-server | |
| req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/login_status?login_token=%s", common.WeChatServerAddress, url.QueryEscape(loginToken)), nil) |
🤖 Prompt for AI Agents
In controller/wechat.go around lines 229 to 239, the code interpolates the
user-supplied loginToken directly into the request URL; URL-encode the
loginToken to prevent malformed URLs or injection by using net/url (e.g.,
url.QueryEscape(loginToken)) or build the URL via url.URL / url.Values and set
the query parameter safely before creating the http.NewRequest; replace the
current fmt.Sprintf(...) interpolation with a safely-encoded query parameter.
| // 轮询登录状态 | ||
| const pollLoginStatus = async (loginToken) => { | ||
| const interval = setInterval(async () => { | ||
| try { | ||
| const res = await API.get(`/api/wechat/login_status?login_token=${loginToken}`); | ||
| if (res.data.success) { | ||
| const { status, auth_code } = res.data.data; | ||
|
|
||
| if (status === 'success' && auth_code) { | ||
| clearInterval(interval); | ||
| setDirectLoginData(prev => ({ ...prev, polling: false })); | ||
|
|
||
| // 使用授权码完成登录 | ||
| await loginWithAuthCode(auth_code); | ||
| } else if (status === 'expired') { | ||
| clearInterval(interval); | ||
| setDirectLoginData(prev => ({ ...prev, polling: false })); | ||
| showError('二维码已过期,请重新生成'); | ||
| } | ||
| } | ||
| } catch (error) { | ||
| console.error('轮询失败:', error); | ||
| } | ||
| }, 2000); // 每2秒轮询一次 | ||
|
|
||
| // 10分钟后停止轮询 | ||
| setTimeout(() => { | ||
| clearInterval(interval); | ||
| setDirectLoginData(prev => ({ ...prev, polling: false })); | ||
| }, 10 * 60 * 1000); | ||
| }; |
There was a problem hiding this comment.
Memory leak: polling interval is not cleaned up on component unmount.
The pollLoginStatus function stores the interval in a local variable that cannot be accessed for cleanup when the component unmounts. If the user navigates away while polling is active, the interval continues running and may cause state updates on an unmounted component.
🔎 Proposed fix using a ref
Add a ref at the component level:
const pollingIntervalRef = useRef(null);
const pollingTimeoutRef = useRef(null);Update the cleanup in a useEffect:
useEffect(() => {
return () => {
if (pollingIntervalRef.current) {
clearInterval(pollingIntervalRef.current);
}
if (pollingTimeoutRef.current) {
clearTimeout(pollingTimeoutRef.current);
}
};
}, []);Then modify pollLoginStatus:
const pollLoginStatus = async (loginToken) => {
- const interval = setInterval(async () => {
+ pollingIntervalRef.current = setInterval(async () => {
try {
const res = await API.get(`/api/wechat/login_status?login_token=${loginToken}`);
if (res.data.success) {
const { status, auth_code } = res.data.data;
if (status === 'success' && auth_code) {
- clearInterval(interval);
+ clearInterval(pollingIntervalRef.current);
+ pollingIntervalRef.current = null;
setDirectLoginData(prev => ({ ...prev, polling: false }));
await loginWithAuthCode(auth_code);
} else if (status === 'expired') {
- clearInterval(interval);
+ clearInterval(pollingIntervalRef.current);
+ pollingIntervalRef.current = null;
setDirectLoginData(prev => ({ ...prev, polling: false }));
showError('二维码已过期,请重新生成');
}
}
} catch (error) {
console.error('轮询失败:', error);
}
}, 2000);
- setTimeout(() => {
- clearInterval(interval);
+ pollingTimeoutRef.current = setTimeout(() => {
+ if (pollingIntervalRef.current) {
+ clearInterval(pollingIntervalRef.current);
+ pollingIntervalRef.current = null;
+ }
setDirectLoginData(prev => ({ ...prev, polling: false }));
}, 10 * 60 * 1000);
};Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In web/src/components/auth/LoginForm.jsx around lines 211 to 241, the polling
interval and timeout are created locally in pollLoginStatus and not cleaned up
on component unmount, causing potential memory leaks and state updates on
unmounted components; fix by adding component-level refs (e.g.,
pollingIntervalRef and pollingTimeoutRef) to store the interval ID and timeout
ID, assign interval/timeout IDs to those refs inside pollLoginStatus instead of
local variables, update pollLoginStatus to check the component is still mounted
before calling setState or use a mounted flag if needed, and add a useEffect
cleanup that clears pollingIntervalRef.current and pollingTimeoutRef.current if
present to ensure timers are cancelled on unmount.
| // 微信登录模态框 | ||
| const renderWeChatLoginModal = () => { | ||
| // 根据 WeChatDirectLoginEnabled 决定显示哪种登录方式 | ||
| const isDirectLogin = status.wechat_direct_login_enabled; | ||
|
|
||
| return ( | ||
| <Modal | ||
| title={isDirectLogin ? t('扫码直接登录') : t('验证码登录')} | ||
| visible={showWeChatLoginModal} | ||
| maskClosable={true} | ||
| onCancel={() => { | ||
| setShowWeChatLoginModal(false); | ||
| setDirectLoginData({ loginToken: '', qrcodeUrl: '', polling: false }); | ||
| }} | ||
| centered={true} | ||
| footer={isDirectLogin ? null : undefined} | ||
| onOk={isDirectLogin ? undefined : onSubmitWeChatVerificationCode} | ||
| okText={isDirectLogin ? undefined : t('登录')} | ||
| okButtonProps={isDirectLogin ? undefined : { | ||
| loading: wechatCodeSubmitLoading, | ||
| }} | ||
| > | ||
| {isDirectLogin ? renderDirectLogin() : renderVerificationLogin()} | ||
| </Modal> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Nested Modal bug when using verification login mode.
When isDirectLogin is false, renderWeChatLoginModal() renders a <Modal> that contains the result of renderVerificationLogin(), which itself returns another <Modal> (lines 891-925). This creates a nested Modal structure.
🔎 Proposed fix
Option 1: Have renderVerificationLogin return only the content, not a Modal:
const renderVerificationLogin = () => {
return (
- <Modal
- title={t('微信扫码登录')}
- visible={showWeChatLoginModal}
- maskClosable={true}
- onOk={onSubmitWeChatVerificationCode}
- onCancel={() => setShowWeChatLoginModal(false)}
- okText={t('登录')}
- centered={true}
- okButtonProps={{
- loading: wechatCodeSubmitLoading,
- }}
- >
+ <>
<div className='flex flex-col items-center mb-4'>
<img src={status.wechat_qrcode} alt='微信二维码' className='mb-4 w-32 h-32' />
</div>
...
- </Modal>
+ </>
);
};Option 2: Remove renderWeChatLoginModal wrapper and call renderVerificationLogin directly in the component return when not using direct login.
Committable suggestion skipped: line range outside the PR's diff.
之前的微信登录, 需要扫码后, 通过公众号获得的验证码登录, 比较繁琐
本次优化, 通过每次生成带自定义值的二维码, 扫码后由服务端实现用户id识别, 避免用户再次输入验证码的流程,实现
扫码即登录原理:
生成带场景值二维码-->用户扫码-->接收微信通知:服务端场景值和openid对应-->前端轮询是否扫码成功-->成功登录
效果:

说明:
由于去掉验证码的功能是基于服务端对二维码场景值和微信openid绑定关系的验证,
因此需要升级原来的wechat-server, 我已提pr给songquanpeng大佬:songquanpeng/wechat-server#22
但由于大佬已不怎么维护此项目, 在它合并之前也可以使用我fork的仓库: https://github.com/feitianbubu/wechat-server
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.