diff --git a/README.md b/README.md index d5db69a7b..3f8235ad5 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,10 @@ pnpm web firefox:zip # output zip file at in .output/md-{version}-firefox.zip # uTools 插件打包 pnpm utools:package # output zip file at apps/utools/release/md-utools-v{version}.zip + +# cloudflare workers +pnpm web wrangler:dev # cloudflare workers dev 模式 +pnpm web wrangler:deploy # cloudflare workers 部署命令 ``` ## 🚀 快速搭建私有服务 diff --git a/apps/web/functions/cgi-bin/material/add_material/index.ts b/apps/web/functions/cgi-bin/material/add_material/index.ts deleted file mode 100644 index 4d79750f8..000000000 --- a/apps/web/functions/cgi-bin/material/add_material/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { parseFormDataRequest } from '../../../parseFormDataRequest' -import { jsonResponse, MP_HOST } from '../../../utils' - -export const onRequestPost: PagesFunction = async (context) => { - const formData = (await parseFormDataRequest(context.request)) as FormData - const url = new URL(context.request.url) - const response = await fetch( - `${MP_HOST}${context.functionPath}${url.search}`, - { - method: `POST`, - body: formData, - }, - ) - const json = await response.json() - return jsonResponse(json) -} diff --git a/apps/web/functions/cgi-bin/media/uploadimg/index.ts b/apps/web/functions/cgi-bin/media/uploadimg/index.ts deleted file mode 100644 index 26a1663ab..000000000 --- a/apps/web/functions/cgi-bin/media/uploadimg/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { onRequestPost as post } from '../../material/add_material/index' - -export const onRequestPost: PagesFunction = async (context) => { - return post(context) -} diff --git a/apps/web/functions/cgi-bin/stable_token.ts b/apps/web/functions/cgi-bin/stable_token.ts deleted file mode 100644 index 8509bf43b..000000000 --- a/apps/web/functions/cgi-bin/stable_token.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { jsonResponse, MP_HOST } from '../utils' - -export const onRequestPost: PagesFunction = async (context) => { - const response = await fetch( - `${MP_HOST}${context.functionPath}`, - { - method: `POST`, - body: context.request.body, - }, - ) - const json = await response.json() - return jsonResponse(json) -} diff --git a/apps/web/functions/parseFormDataRequest.ts b/apps/web/functions/parseFormDataRequest.ts deleted file mode 100644 index 974fd0968..000000000 --- a/apps/web/functions/parseFormDataRequest.ts +++ /dev/null @@ -1,40 +0,0 @@ -// eslint-disable-next-line ts/ban-ts-comment -// @ts-nocheck -// @see https://github.com/cloudflare/images.pages.dev/blob/main/functions/utils/parseFormDataRequest.ts -import { parseMultipart } from '@ssttevee/multipart-parser' - -const RE_MULTIPART - = /^multipart\/form-data;\s*boundary=(?:"((?:[^"]|\\")+)"|([^\s;]+))$/ - -function getBoundary(request: Request): string | undefined { - const contentType = request.headers.get(`Content-Type`) - if (!contentType) - return - - const matches = RE_MULTIPART.exec(contentType) - if (!matches) - return - - return matches[1] || matches[2] -} - -export async function parseFormDataRequest(request: Request): Promise { - const boundary = getBoundary(request) - if (!boundary || !request.body) - return - - const parts = await parseMultipart(request.body, boundary) - - const formData = new FormData() - - for (const { name, data, filename, contentType } of parts) { - formData.append( - name, - filename - ? new File([data], filename, { type: contentType }) - : new TextDecoder().decode(data), - ) - } - - return formData -} diff --git a/apps/web/functions/utils.ts b/apps/web/functions/utils.ts deleted file mode 100644 index b5b0d9bd4..000000000 --- a/apps/web/functions/utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const MP_HOST = `https://api.weixin.qq.com` - -export function jsonResponse(value: any, init: ResponseInit = {}) { - return new Response(JSON.stringify(value), { - headers: { 'Content-Type': `application/json`, ...init.headers }, - ...init, - }) -} diff --git a/apps/web/package.json b/apps/web/package.json index 31f86105a..fdb90494f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,6 +14,8 @@ "build:analyze": "cross-env ANALYZE=true vite build", "compile:extension": "pnpm --prefix ./src/extension run compile", "preview": "pnpm run build && vite preview", + "wrangler:dev": "cross-env CF_PAGES=1 pnpm run build:h5-netlify && wrangler dev", + "wrangler:deploy": "cross-env CF_PAGES=1 pnpm run build:h5-netlify && wrangler deploy", "preview:pages": "cross-env CF_PAGES=1 pnpm run build:h5-netlify && wrangler pages dev ./dist", "release:cli": "node ./scripts/release.js", "ext:dev": "wxt", diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 31a62df21..ab6a93263 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -11,6 +11,9 @@ }, { "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.worker.json" } ], "files": [] diff --git a/apps/web/functions/tsconfig.json b/apps/web/tsconfig.worker.json similarity index 51% rename from apps/web/functions/tsconfig.json rename to apps/web/tsconfig.worker.json index d4b8ff9c5..85c173d5c 100644 --- a/apps/web/functions/tsconfig.json +++ b/apps/web/tsconfig.worker.json @@ -1,8 +1,6 @@ { + "extends": "./tsconfig.node.json", "compilerOptions": { - "target": "esnext", - "lib": ["esnext"], - "module": "esnext", "types": ["@cloudflare/workers-types"] } } diff --git a/apps/web/worker/index.ts b/apps/web/worker/index.ts new file mode 100644 index 000000000..476948981 --- /dev/null +++ b/apps/web/worker/index.ts @@ -0,0 +1,58 @@ +import { WorkerEntrypoint } from 'cloudflare:workers' + +const MP_HOST = `https://api.weixin.qq.com` + +export default class extends WorkerEntrypoint { + async fetch(request: Request): Promise { + // 1️⃣ 获取原请求 URL 与路径 + const url = new URL(request.url) + + // 拼接转发目标,例如请求 /cgi-bin/stable_token 就会转发到 + // https://api.weixin.qq.com/cgi-bin/stable_token + const targetUrl = `${MP_HOST}${url.pathname}${url.search}` + + // 2️⃣ 克隆请求头 + const headers = new Headers(request.headers) + + // 可选:删除或修改一些可能引起冲突的头 + headers.delete(`host`) + headers.delete(`content-length`) + headers.delete(`cf-connecting-ip`) + headers.delete(`x-forwarded-for`) + + // 3️⃣ 构造新的请求 + const init: RequestInit = { + method: request.method, + headers, + redirect: `follow`, + } + + // ⚙️ 特别处理带 body 的请求(POST/PUT 等) + if (request.method !== `GET` && request.method !== `HEAD`) { + // 对文件上传、JSON、表单都可直接转发 + init.body = request.body + } + + try { + // 4️⃣ 发起转发请求 + const resp = await fetch(targetUrl, init) + + // 5️⃣ 克隆返回的响应头 + const respHeaders = new Headers(resp.headers) + // 可选:允许跨域访问(如果你需要在网页端调用) + respHeaders.set(`Access-Control-Allow-Origin`, `*`) + respHeaders.set(`Access-Control-Allow-Headers`, `*`) + + return new Response(resp.body, { + status: resp.status, + headers: respHeaders, + }) + } + catch (err: any) { + return new Response(JSON.stringify({ error: err.message }), { + status: 500, + headers: { 'Content-Type': `application/json` }, + }) + } + } +} diff --git a/apps/web/wrangler.jsonc b/apps/web/wrangler.jsonc new file mode 100644 index 000000000..8561f387d --- /dev/null +++ b/apps/web/wrangler.jsonc @@ -0,0 +1,51 @@ +/** + * For more details on how to configure Wrangler, refer to: + * https://developers.cloudflare.com/workers/wrangler/configuration/ + */ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "md-web", + "compatibility_date": "2025-09-06", + "main": "worker/index.ts", + "assets": { + "not_found_handling": "single-page-application", + "directory": "./dist/" + }, + "observability": { + "enabled": true + }, + "compatibility_flags": [ + "nodejs_compat", + "nodejs_compat_populate_process_env" + ] + /** + * Smart Placement + * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement + */ + // "placement": { "mode": "smart" } + /** + * Bindings + * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including + * databases, object storage, AI inference, real-time communication and more. + * https://developers.cloudflare.com/workers/runtime-apis/bindings/ + */ + /** + * Environment Variables + * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables + */ + // "vars": { "MY_VARIABLE": "production_value" } + /** + * Note: Use secrets to store sensitive data. + * https://developers.cloudflare.com/workers/configuration/secrets/ + */ + /** + * Static Assets + * https://developers.cloudflare.com/workers/static-assets/binding/ + */ + // "assets": { "directory": "./public/", "binding": "ASSETS" } + /** + * Service Bindings (communicate between multiple Workers) + * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + */ + // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] +}