Files
wechat-article-exporter/server/utils/proxy-request.ts
2026-02-24 16:42:51 +08:00

162 lines
5.2 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import dayjs from 'dayjs';
import { H3Event, parseCookies } from 'h3';
import { v4 as uuidv4 } from 'uuid';
import { isDev, USER_AGENT } from '~/config';
import { RequestOptions } from '~/server/types';
import { cookieStore, getCookieFromStore } from '~/server/utils/CookieStore';
import { logRequest, logResponse } from '~/server/utils/logger';
/**
* 代理微信公众号请求
* @description 备注:只有登录请求(`action=login`)中的 `set-cookie` 才会被写入到 CookieStore 中
* @param options 请求参数
*/
export async function proxyMpRequest(options: RequestOptions) {
const runtimeConfig = useRuntimeConfig();
const headers = new Headers({
Referer: 'https://mp.weixin.qq.com/',
Origin: 'https://mp.weixin.qq.com',
'User-Agent': USER_AGENT,
'Accept-Encoding': 'identity', // 禁用压缩避免出现response.clone() bug
});
// 优先读取参数中的 cookie若无则从 CookieStore 中读取
const cookie: string | null = options.cookie || (await getCookieFromStore(options.event));
if (cookie) {
headers.set('Cookie', cookie);
}
const requestInit: RequestInit = {
method: options.method,
headers: headers,
redirect: options.redirect || 'follow',
};
// 处理参数
if (options.query) {
options.endpoint += '?' + new URLSearchParams(options.query as Record<string, string>).toString();
}
if (options.method === 'POST' && options.body) {
requestInit.body = new URLSearchParams(options.body as Record<string, string>).toString();
}
// 构造请求
const request = new Request(options.endpoint, requestInit);
// 记录请求报文
const requestId = uuidv4().replace(/-/g, '');
if (process.env.NUXT_DEBUG_MP_REQUEST && isDev) {
await logRequest(requestId, request.clone());
}
// 转发请求
const mpResponse = await fetch(request);
// 记录响应报文
if (process.env.NUXT_DEBUG_MP_REQUEST && isDev) {
await logResponse(requestId, mpResponse.clone());
}
let setCookies: string[] = [];
// 处理登录请求的 uuid cookie
if (options.action === 'start_login') {
// 提取出 uuid 这个 cookie并透传给客户端
setCookies = mpResponse.headers.getSetCookie().filter(cookie => cookie.startsWith('uuid='));
}
// 处理登录成功请求的 cookie
// 只有登录请求才会将 Cookie 数据写入 CookieStore
// 返回给客户端的一个 auth-key 的 cookie
else if (options.action === 'login') {
// 提取出 token 和 cookies
try {
const authKey = crypto.randomUUID().replace(/-/g, '');
const body = await mpResponse.clone().json();
const redirectUrl = body?.redirect_url;
if (!redirectUrl || typeof redirectUrl !== 'string') {
throw new Error(`登录响应中未找到 redirect_url响应内容: ${JSON.stringify(body)}`);
}
const token = new URL(`http://localhost${redirectUrl}`).searchParams.get('token');
if (!token) {
throw new Error(`redirect_url 中未找到 token 参数: ${redirectUrl}`);
}
console.log('token', token);
const success = await cookieStore.setCookie(authKey, token, mpResponse.headers.getSetCookie());
if (!success) {
throw new Error('cookie 写入 KV 存储失败');
}
console.log('cookie 写入成功');
setCookies = [
`auth-key=${authKey}; Path=/; Expires=${dayjs().add(4, 'days').toString()}; Secure; HttpOnly`,
// 登录成功后,删除浏览器的 uuid cookie
`uuid=EXPIRED; Path=/; Expires=${dayjs().subtract(1, 'days').toString()}; Secure; HttpOnly`,
];
} catch (error) {
console.error('action(login) failed:', error);
// 登录失败时返回错误响应,而不是静默继续
return new Response(JSON.stringify({ base_resp: { ret: -1, err_msg: `登录处理失败: ${error}` } }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
}
// 处理切换公众号的请求
else if (options.action === 'switch_account') {
const authKey = getAuthKeyFromRequest(options.event);
if (authKey) {
setCookies = ['switch_account=1'];
}
}
// 这里是否需要执行?
// 更新 CookieStore 中的 cookie
else {
// updateCookies(options.event, mpResponse.headers.getSetCookie());
}
// 构造返回给客户端的响应
const responseHeaders = new Headers(mpResponse.headers);
responseHeaders.delete('set-cookie');
setCookies.forEach(setCookie => {
responseHeaders.append('set-cookie', setCookie);
});
const finalResponse = new Response(mpResponse.body, {
status: mpResponse.status,
statusText: mpResponse.statusText,
headers: responseHeaders,
});
if (!options.parseJson) {
return finalResponse;
} else {
return finalResponse.json();
}
}
export function getAuthKeyFromRequest(event: H3Event): string {
let authKey = getRequestHeader(event, 'X-Auth-Key');
if (!authKey) {
const cookies = parseCookies(event);
authKey = cookies['auth-key'];
}
return authKey;
}
// function updateCookies(event: H3Event, cookies: string[]): void {
// const authKey = getAuthKeyFromRequest(event);
// if (authKey) {
// cookieStore.updateCookie(authKey, cookies);
// }
// }