mirror of
https://github.com/wechat-article/wechat-article-exporter.git
synced 2026-06-01 17:11:46 +08:00
162 lines
5.2 KiB
TypeScript
162 lines
5.2 KiB
TypeScript
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);
|
||
// }
|
||
// }
|