Files
pixivnow/functions/[[path]].ts
JuLuogo 0faea53617 feat: 添加前端页面处理和静态资源支持
- 移除测试端点,添加前端页面处理逻辑
- 实现静态资源处理功能
- 添加SPA路由支持,所有未匹配路径返回前端页面
- 更新依赖,添加@cloudflare/kv-asset-handler
2025-10-08 16:15:24 +08:00

582 lines
19 KiB
TypeScript
Raw 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.

// 手动实现 CORS 处理
function corsHeaders() {
return {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With',
'Access-Control-Max-Age': '86400',
}
}
function corsResponse(response: Response) {
const headers = new Headers(response.headers)
Object.entries(corsHeaders()).forEach(([key, value]) => {
headers.set(key, value)
})
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
})
}
export default {
fetch: async (request: Request, env: any, ctx: any) => {
console.log('Worker fetch called:', request.method, request.url)
const url = new URL(request.url)
const path = url.pathname
// 处理 OPTIONS 请求CORS 预检)
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 200,
headers: corsHeaders(),
})
}
try {
// 随机图片 API
if (path === '/api/illust/random') {
return await handleRandomAPI(request, env, url)
}
// 通用代理 (ajax|rpc)
if (path.match(/^\/(ajax|rpc)\//)) {
return await handleGenericProxy(request, env)
}
// 图片代理 (~|-)
if (path.match(/^\/[~-]\//)) {
return await handleImageProxy(request, env)
}
// 用户 API
if (path === '/api/user') {
return await handleUserAPI(request, env, url)
}
// 静态资源处理
if (path.startsWith('/assets/') || path === '/favicon.ico' || path === '/robots.txt' || path.startsWith('/images/')) {
return await handleStaticAssets(request, env, path)
}
// 根路径和其他路径都返回前端页面SPA 路由)
return await handleFrontendPage(request, env)
} catch (error) {
console.error('Worker error:', error)
return corsResponse(new Response(JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
}), {
status: 500,
headers: { 'Content-Type': 'application/json' },
}))
}
},
}
// 随机图片 API 处理器
async function handleRandomAPI(request: Request, env: any, url: URL) {
try {
// 简化版本:返回模拟数据而不是调用 Pixiv API
const requestImage =
(request.headers.get('accept')?.includes('image') || url.searchParams.get('format') === 'image') &&
url.searchParams.get('format') !== 'json'
// 模拟数据用于测试
const mockIllusts = [
{
id: '123456789',
title: 'Test Illustration',
userId: '987654321',
userName: 'Test Artist',
tags: ['test', 'mock'],
updateDate: '2024-01-01T12:00:00+00:00',
urls: {
mini: 'https://i.pximg.net/c/48x48/img-master/img/2024/01/01/12/00/00/123456789_p0_square1200.jpg',
thumb: 'https://i.pximg.net/c/250x250_80_a2/img-master/img/2024/01/01/12/00/00/123456789_p0_square1200.jpg',
small: 'https://i.pximg.net/c/540x540_70/img-master/img/2024/01/01/12/00/00/123456789_p0_master1200.jpg',
regular: 'https://i.pximg.net/img-master/img/2024/01/01/12/00/00/123456789_p0_master1200.jpg',
original: 'https://i.pximg.net/img-original/img/2024/01/01/12/00/00/123456789_p0.jpg',
}
}
]
if (requestImage && mockIllusts[0]?.urls?.regular) {
return corsResponse(new Response(null, {
status: 302,
headers: { Location: mockIllusts[0].urls.regular }
}))
}
return corsResponse(new Response(JSON.stringify(mockIllusts), {
headers: { 'Content-Type': 'application/json' },
}))
/* 原始 Pixiv API 调用代码 - 暂时注释掉
const pixivUrl = new URL('https://www.pixiv.net/ajax/illust/discovery')
pixivUrl.searchParams.set('mode', url.searchParams.get('mode') ?? 'safe')
pixivUrl.searchParams.set('max', requestImage ? '1' : url.searchParams.get('max') ?? '18')
const headers = new Headers()
headers.set('referer', 'https://www.pixiv.net/')
// 使用默认的 User-Agent或者环境变量中的自定义 User-Agent
// 使用默认的 User-Agent或者环境变量中的自定义 User-Agent
headers.set('user-agent', env.USER_AGENT || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0')
console.log('Fetching from Pixiv API:', pixivUrl.toString())
const response = await fetch(pixivUrl.toString(), { headers })
if (!response.ok) {
console.error('Pixiv API error:', response.status, response.statusText)
return corsResponse(new Response(JSON.stringify({ error: `Pixiv API returned ${response.status}` }), {
status: response.status,
headers: { 'Content-Type': 'application/json' },
}))
}
const data = await response.json()
console.log('Pixiv API response:', data)
// 检查 API 响应是否有错误
if (data.error) {
console.error('Pixiv API returned error:', data.error)
return corsResponse(new Response(JSON.stringify({ error: data.error }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
}))
}
const illusts = (data.illusts ?? []).filter((value: any) =>
value && typeof value === 'object' && value.id
)
if (illusts.length === 0) {
return corsResponse(new Response(JSON.stringify([]), {
headers: { 'Content-Type': 'application/json' },
}))
}
// 使用默认的 Pixiv 图片 URL或者环境变量中的自定义代理 URL
const PXIMG_BASEURL_I = (env.VITE_PXIMG_BASEURL_I || 'https://i.pximg.net/').replace(/\/$/, '') + '/'
// 处理图片 URL
illusts.forEach((value: any) => {
try {
if (value.updateDate) {
const date = new Date(value.updateDate)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
const second = String(date.getSeconds()).padStart(2, '0')
const middle = `img/${year}/${month}/${day}/${hour}/${minute}/${second}/${value.id}`
value.urls = {
mini: `${PXIMG_BASEURL_I}c/48x48/img-master/${middle}_p0_square1200.jpg`,
thumb: `${PXIMG_BASEURL_I}c/250x250_80_a2/img-master/${middle}_p0_square1200.jpg`,
small: `${PXIMG_BASEURL_I}c/540x540_70/img-master/${middle}_p0_master1200.jpg`,
regular: `${PXIMG_BASEURL_I}img-master/${middle}_p0_master1200.jpg`,
original: `${PXIMG_BASEURL_I}img-original/${middle}_p0.jpg`,
}
} else {
const middle = `img/2024/01/01/00/00/00/${value.id}`
value.urls = {
mini: `${PXIMG_BASEURL_I}c/48x48/img-master/${middle}_p0_square1200.jpg`,
thumb: `${PXIMG_BASEURL_I}c/250x250_80_a2/img-master/${middle}_p0_square1200.jpg`,
small: `${PXIMG_BASEURL_I}c/540x540_70/img-master/${middle}_p0_master1200.jpg`,
regular: `${PXIMG_BASEURL_I}img-master/${middle}_p0_master1200.jpg`,
original: `${PXIMG_BASEURL_I}img-original/${middle}_p0.jpg`,
}
}
} catch (error) {
console.error('Error processing illust:', value.id, error)
const middle = `img/2024/01/01/00/00/00/${value.id}`
value.urls = {
mini: `${PXIMG_BASEURL_I}c/48x48/img-master/${middle}_p0_square1200.jpg`,
thumb: `${PXIMG_BASEURL_I}c/250x250_80_a2/img-master/${middle}_p0_square1200.jpg`,
small: `${PXIMG_BASEURL_I}c/540x540_70/img-master/${middle}_p0_master1200.jpg`,
regular: `${PXIMG_BASEURL_I}img-master/${middle}_p0_master1200.jpg`,
original: `${PXIMG_BASEURL_I}img-original/${middle}_p0.jpg`,
}
}
})
if (requestImage && illusts[0]?.urls?.regular) {
return corsResponse(new Response(null, {
status: 302,
headers: { Location: illusts[0].urls.regular }
}))
}
return corsResponse(new Response(JSON.stringify(illusts), {
headers: { 'Content-Type': 'application/json' },
}))
*/
} catch (error) {
console.error('Error in random API:', error)
return corsResponse(new Response(JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
}), {
status: 500,
headers: { 'Content-Type': 'application/json' },
}))
}
}
// 通用代理处理器
async function handleGenericProxy(request: Request, env: any) {
const url = new URL(request.url)
url.hostname = 'www.pixiv.net'
const headers = new Headers(request.headers)
headers.set('origin', 'https://www.pixiv.net')
headers.set('referer', 'https://www.pixiv.net/')
// 使用默认的 User-Agent或者环境变量中的自定义 User-Agent
headers.set('user-agent', env.USER_AGENT || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0')
const newReq = new Request(url.toString(), {
method: request.method,
headers,
body: request.body,
})
const response = await fetch(newReq)
return corsResponse(response)
}
// 图片代理处理器
async function handleImageProxy(request: Request, env: any) {
const url = new URL(request.url)
const path = url.pathname.slice(2)
// 使用环境变量配置的反代 URL或者默认的 Pixiv 原始 URL
if (url.pathname.startsWith('/~')) {
// 处理 s.pximg.net 的图片
if (env.VITE_PXIMG_BASEURL_S) {
url.href = env.VITE_PXIMG_BASEURL_S + path
} else {
url.hostname = 's.pximg.net'
url.pathname = '/' + path
}
} else {
// 处理 i.pximg.net 的图片
if (env.VITE_PXIMG_BASEURL_I) {
url.href = env.VITE_PXIMG_BASEURL_I + path
} else {
url.hostname = 'i.pximg.net'
url.pathname = '/' + path
}
}
const headers = new Headers()
for (const h of ['accept', 'accept-encoding', 'accept-language', 'cache-control', 'user-agent']) {
if (request.headers.get(h)) {
headers.set(h, request.headers.get(h)!)
}
}
headers.set('referer', 'https://www.pixiv.net/')
// 使用默认的 User-Agent或者环境变量中的自定义 User-Agent
headers.set('user-agent', env.USER_AGENT || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0')
const newReq = new Request(url.toString(), {
headers,
})
const response = await fetch(newReq)
return corsResponse(response)
}
// 用户 API 处理器
async function handleUserAPI(request: Request, env: any, url: URL) {
try {
const userId = url.searchParams.get('id')
if (!userId) {
return corsResponse(new Response(JSON.stringify({ error: 'User ID is required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
}))
}
const pixivUrl = new URL(`https://www.pixiv.net/ajax/user/${userId}`)
const headers = new Headers()
headers.set('referer', 'https://www.pixiv.net/')
// 使用默认的 User-Agent或者环境变量中的自定义 User-Agent
headers.set('user-agent', env.USER_AGENT || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0')
const response = await fetch(pixivUrl.toString(), { headers })
if (!response.ok) {
return corsResponse(new Response(JSON.stringify({ error: `Pixiv API returned ${response.status}` }), {
status: response.status,
headers: { 'Content-Type': 'application/json' },
}))
}
const data = await response.json()
return corsResponse(new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
}))
} catch (error) {
console.error('Error in user API:', error)
return corsResponse(new Response(JSON.stringify({
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
}), {
status: 500,
headers: { 'Content-Type': 'application/json' },
}))
}
}
// 静态资源处理器
async function handleStaticAssets(request: Request, env: any, path: string) {
try {
const url = new URL(request.url)
const pathname = url.pathname
// 根据文件扩展名设置 Content-Type
let contentType = 'text/plain'
if (pathname.endsWith('.js')) {
contentType = 'application/javascript'
} else if (pathname.endsWith('.css')) {
contentType = 'text/css'
} else if (pathname.endsWith('.ico')) {
contentType = 'image/x-icon'
} else if (pathname.endsWith('.svg')) {
contentType = 'image/svg+xml'
} else if (pathname.endsWith('.png')) {
contentType = 'image/png'
} else if (pathname.endsWith('.jpg') || pathname.endsWith('.jpeg')) {
contentType = 'image/jpeg'
}
// 尝试读取文件
try {
// 这里需要使用 Cloudflare Workers 的文件系统 API
// 由于 Workers 环境限制,我们需要将文件内容嵌入到代码中
// 或者使用 KV 存储
return new Response('Static asset not found', {
status: 404,
headers: { 'Content-Type': 'text/plain' }
})
} catch (e) {
return new Response('Static asset not found', {
status: 404,
headers: { 'Content-Type': 'text/plain' }
})
}
} catch (e) {
return new Response('Static asset not found', {
status: 404,
headers: { 'Content-Type': 'text/plain' }
})
}
}
// 前端页面处理器
async function handleFrontendPage(request: Request, env: any) {
// 返回前端 HTML 页面
const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PixivNow</title>
<!-- Umami Analytics -->
<script defer src="https://cloud.umami.is/script.js" data-website-id="842d980c-5e11-4834-a2a8-5daaa285ce66"></script>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
text-align: center;
color: white;
max-width: 600px;
padding: 2rem;
}
.logo {
font-size: 3rem;
font-weight: bold;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.subtitle {
font-size: 1.2rem;
margin-bottom: 2rem;
opacity: 0.9;
}
.search-box {
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50px;
padding: 1rem 2rem;
font-size: 1rem;
color: white;
width: 100%;
max-width: 400px;
margin: 0 auto 2rem;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.search-box::placeholder {
color: rgba(255, 255, 255, 0.7);
}
.search-box:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.2);
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 2rem;
}
.feature {
background: rgba(255, 255, 255, 0.1);
padding: 1.5rem;
border-radius: 15px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.feature h3 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
}
.feature p {
margin: 0;
opacity: 0.8;
font-size: 0.9rem;
}
.status {
margin-top: 2rem;
padding: 1rem;
background: rgba(0, 255, 0, 0.1);
border: 1px solid rgba(0, 255, 0, 0.3);
border-radius: 10px;
color: #90EE90;
}
.api-info {
margin-top: 2rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
font-size: 0.9rem;
opacity: 0.8;
}
.api-info a {
color: #FFD700;
text-decoration: none;
}
.api-info a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">PixivNow</div>
<div class="subtitle">探索精彩的 Pixiv 作品世界</div>
<input type="text" class="search-box" placeholder="输入关键词或画师名称搜索作品..." />
<div class="features">
<div class="feature">
<h3>🎨 随机作品</h3>
<p>发现意想不到的精彩作品</p>
</div>
<div class="feature">
<h3>🔍 智能搜索</h3>
<p>快速找到你喜欢的内容</p>
</div>
<div class="feature">
<h3>📱 响应式设计</h3>
<p>完美适配各种设备</p>
</div>
<div class="feature">
<h3>⚡ 高速访问</h3>
<p>基于 Cloudflare 全球加速</p>
</div>
</div>
<div class="status">
✅ 服务正常运行中
</div>
<div class="api-info">
<p>API 接口可用:</p>
<p><a href="/api/illust/random">/api/illust/random</a> - 随机作品</p>
<p><a href="/api/user">/api/user</a> - 用户信息</p>
<p>图片代理:<code>/i/</code> 和 <code>/s/</code></p>
</div>
</div>
<script>
// 简单的搜索功能演示
document.querySelector('.search-box').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const query = this.value.trim();
if (query) {
alert('搜索功能正在开发中,敬请期待!\\n搜索关键词' + query);
}
}
});
// 添加一些交互效果
document.querySelectorAll('.feature').forEach(feature => {
feature.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-5px)';
this.style.boxShadow = '0 10px 20px rgba(0,0,0,0.2)';
});
feature.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
this.style.boxShadow = 'none';
});
});
</script>
</body>
</html>`
return corsResponse(new Response(html, {
headers: { 'Content-Type': 'text/html; charset=utf-8' },
}))
}