diff --git a/AGENTS.md b/AGENTS.md
index c54b854d..f5e8a22a 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -12,6 +12,7 @@
- 默认使用中文说明,优先自然语言,少量必要代码示例。
- 先做只读摸底,再决定是否修改。
+- 非必要不要使用兜底逻辑;优先把原生主链路和标准流程做好,只有在明确需要兼容、降级或应急,并说明原因和影响范围时,才增加兜底处理。
- 修改后必须验证,先跑最相关的验证,再视情况扩大范围。
- 不触碰其他应用配置,不把项目规则写入全局 `Codex` 配置。
diff --git a/app/api/dingtalk/h5-login/route.ts b/app/api/dingtalk/h5-login/route.ts
index 6846d29d..ec9241e3 100644
--- a/app/api/dingtalk/h5-login/route.ts
+++ b/app/api/dingtalk/h5-login/route.ts
@@ -1,13 +1,201 @@
import { NextRequest, NextResponse } from 'next/server'
import { loginDingtalkUserByH5AuthCode } from '@/lib/dingtalk-login'
+import {
+ buildDingtalkAutoLoginPath,
+ normalizeInternalRedirectPath,
+} from '@/lib/safe-redirect'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
+function appendLoginSetCookie(target: NextResponse, source: Response) {
+ const getSetCookie = (source.headers as any).getSetCookie
+ const cookies = typeof getSetCookie === 'function'
+ ? getSetCookie.call(source.headers)
+ : [source.headers.get('set-cookie')].filter(Boolean)
+
+ for (const cookie of cookies) {
+ target.headers.append('set-cookie', cookie)
+ }
+}
+
+function createCookieLandingResponse({
+ redirectPath,
+ loginResponse,
+ title = '登录成功',
+ message = '正在进入考核调研...',
+}: {
+ redirectPath: string
+ loginResponse: Response
+ title?: string
+ message?: string
+}) {
+ const escapedPath = JSON.stringify(redirectPath).replace(/
+
+
+
+
+ 正在进入素材库
+
+
+
+
+
+
+
+
+
+
+`,
+ {
+ status: 200,
+ headers: {
+ 'Content-Type': 'text/html; charset=utf-8',
+ 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
+ },
+ },
+ )
+
+ appendLoginSetCookie(response, loginResponse)
+ return response
+}
+
+function createH5LoginErrorPage(message: string, redirectPath: string) {
+ const safeMessage = JSON.stringify(message || '钉钉免登失败,请联系管理员检查配置').replace(/
+
+
+
+
+ 钉钉免登失败
+
+
+
+
+
+ 钉钉免登失败
+
+ 改用网页授权登录
+
+
+
+`,
+ {
+ status: 401,
+ headers: {
+ 'Content-Type': 'text/html; charset=utf-8',
+ 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
+ },
+ },
+ )
+}
+
+async function readH5LoginCode(request: NextRequest) {
+ const contentType = request.headers.get('content-type') || ''
+ if (contentType.includes('application/json')) {
+ const body = await request.json().catch(() => ({}))
+ return String(body?.code || '').trim()
+ }
+
+ if (
+ contentType.includes('application/x-www-form-urlencoded') ||
+ contentType.includes('multipart/form-data')
+ ) {
+ const body = await request.formData().catch(() => null)
+ return String(body?.get('code') || '').trim()
+ }
+
+ const body = await request.text().catch(() => '')
+ return new URLSearchParams(body).get('code')?.trim() || ''
+}
+
+function isHtmlNavigationRequest(request: NextRequest) {
+ const accept = request.headers.get('accept') || ''
+ const fetchMode = request.headers.get('sec-fetch-mode') || ''
+ return accept.includes('text/html') || fetchMode === 'navigate'
+}
+
+async function handleH5LoginPage(request: NextRequest, code: string) {
+ const redirectPath = normalizeInternalRedirectPath(
+ request.nextUrl.searchParams.get('redirect'),
+ '/performance-survey',
+ )
+
+ if (!code) {
+ return createH5LoginErrorPage('缺少钉钉免登授权码,请从钉钉工作通知重新打开', redirectPath)
+ }
+
+ try {
+ const loginResponse = await loginDingtalkUserByH5AuthCode(request, code, {
+ appSource: 'workNotice',
+ })
+ const data = await loginResponse.json().catch(() => null)
+
+ if (!loginResponse.ok || !data?.success) {
+ return createH5LoginErrorPage(data?.message || '钉钉登录未能创建素材库会话', redirectPath)
+ }
+
+ if (data.needCompleteProfile) {
+ const userInfo = encodeURIComponent(JSON.stringify(data.user || {}))
+ return createCookieLandingResponse({
+ redirectPath: `/complete-profile?userInfo=${userInfo}`,
+ loginResponse,
+ title: '首次登录',
+ message: '正在进入资料完善页面...',
+ })
+ }
+
+ return createCookieLandingResponse({ redirectPath, loginResponse })
+ } catch (error: any) {
+ console.error('钉钉H5免登页面跳转失败:', error)
+ return createH5LoginErrorPage(error.message || '钉钉免登失败,请稍后再试', redirectPath)
+ }
+}
+
+export async function GET(request: NextRequest) {
+ const code = String(
+ request.nextUrl.searchParams.get('code') ||
+ request.nextUrl.searchParams.get('authCode') ||
+ '',
+ ).trim()
+
+ return handleH5LoginPage(request, code)
+}
+
export async function POST(request: NextRequest) {
try {
- const body = await request.json().catch(() => ({}))
- const code = String(body?.code || '').trim()
+ const code = await readH5LoginCode(request)
+
+ if (isHtmlNavigationRequest(request)) {
+ return handleH5LoginPage(request, code)
+ }
if (!code) {
return NextResponse.json(
diff --git a/app/dingtalk-entry/route.ts b/app/dingtalk-entry/route.ts
index 37d8c6b3..ff39799b 100644
--- a/app/dingtalk-entry/route.ts
+++ b/app/dingtalk-entry/route.ts
@@ -146,36 +146,19 @@ function createH5LoginPage({
function submitCode(code) {
setStatus('已获取钉钉身份,正在创建登录状态...');
- fetch('/api/dingtalk/h5-login', {
- method: 'POST',
- credentials: 'same-origin',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ code: code })
- })
- .then(function (response) {
- return response.json().catch(function () { return {}; }).then(function (data) {
- return { ok: response.ok, data: data };
- });
- })
- .then(function (result) {
- if (!result.ok || !result.data || !result.data.success) {
- throw new Error((result.data && result.data.message) || '登录失败');
- }
+ var form = document.createElement('form');
+ form.method = 'POST';
+ form.action = '/api/dingtalk/h5-login?redirect=' + encodeURIComponent(target);
+ form.style.display = 'none';
- if (result.data.needCompleteProfile) {
- setStatus('首次登录,需要完善账号信息...');
- completeProfile(result.data.user);
- return;
- }
+ var input = document.createElement('input');
+ input.type = 'hidden';
+ input.name = 'code';
+ input.value = code;
+ form.appendChild(input);
- setStatus('登录成功,正在打开考核调研...');
- setTimeout(function () {
- window.location.replace(target);
- }, 350);
- })
- .catch(function (error) {
- showError(error && error.message ? error.message : '钉钉免登失败');
- });
+ document.body.appendChild(form);
+ form.submit();
}
function resolveAuthCode(result) {