Files
DDNS/docs/esa.js
T
aa54605379 doc(esa): Add esa config (#650)
* Update esa.jsonc for new project configuration

* Add legacy doc redirects in VitePress and simplify ESA 404 handling (#649)

* Initial plan

* docs: add legacy redirects and simplify 404 handling

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): simplify legacy redirect logic

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): clarify doc redirects

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): align clean url handling

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): tidy url constants

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): reuse en base constant

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): use constants for en base

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): respect dynamic en base

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): reuse html extension constant

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): streamline doc redirect logic

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): further simplify legacy redirects

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): inline clean-url handling

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): avoid redundant html suffix

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* chore(docs): coalesce clean-url handling

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* Improve URL target resolution logic

Refactor URL target resolution to handle clean URLs.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>
Co-authored-by: New Future <NewFuture@users.noreply.github.com>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-14 12:49:08 +00:00

212 lines
7.8 KiB
JavaScript
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.
/**
* 阿里云ESA边缘函数 - DDNS Release Proxy Script
* Alibaba Cloud ESA Edge Function - DDNS Release Proxy
*
* 功能说明 (Features):
* - 统一格式代理: /releases/{version}/{binary_file}
* - 版本可以是具体版本号(如 v4.1.3-beta1)、latest 或 beta
* - 具体版本: 无限缓存 | latest: 12小时缓存 | beta: 转换为具体版本后无限缓存
* - 流式传输: 直接返回响应流,先缓存后返回
*
* Unified format: /releases/{version}/{binary_file}
* - version can be specific version (e.g., v4.1.3-beta1), "latest", or "beta"
* - Specific versions: infinite cache | latest: 12-hour cache | beta: infinite cache after conversion to specific version
* - Streaming: Returns response stream directly, cache first then return
*
* 使用方法 (Usage):
* 1. 在阿里云ESA控制台创建边缘函数
* 2. 复制此代码到函数编辑器
* 3. 配置路由规则匹配 /releases/*
* 4. 创建EdgeKV命名空间用于存储beta版本映射
*
* 示例 (Examples):
* - https://your-domain.com/releases/v4.1.3-beta1/ddns-windows-x64.exe
* - https://your-domain.com/releases/latest/ddns-glibc-linux_amd64
* - https://your-domain.com/releases/beta/ddns-glibc-linux_amd64
*/
const GITHUB_REPO = 'NewFuture/DDNS';
const edgeKv = new EdgeKV({ namespace: 'ddns-releases' });
/**
* 获取最新beta版本 (Get latest beta version)
* @returns {Promise<string>} beta版本号 (Beta version number)
*/
async function getLatestBetaVersion() {
// 尝试从EdgeKV获取缓存的beta版本 (Try to get cached beta version from EdgeKV)
const cacheKey = 'ddns-beta-version';
try {
const cachedVersion = await edgeKv.get(cacheKey);
if (cachedVersion) {
return cachedVersion;
}
} catch (err) {
console.log('EdgeKV get error:', err);
}
// 从GitHub API获取最新的beta版本 (Fetch latest beta version from GitHub API)
const apiUrl = `https://api.github.com/repos/${GITHUB_REPO}/releases`;
const apiResponse = await fetch(apiUrl, {
headers: {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'ESA-Edge-Function'
}
});
if (apiResponse.ok) {
const releases = await apiResponse.json();
// 查找第一个prerelease或tag_name包含beta的release (Find first prerelease or release with beta in tag_name)
for (const release of releases) {
if ((release.prerelease || (release.tag_name && release.tag_name.includes('beta'))) && release.tag_name) {
// 缓存到EdgeKV,6小时过期 (Cache to EdgeKV, 6-hour expiration)
try {
await edgeKv.put(cacheKey, release.tag_name, { expiration_ttl: 21600 });
} catch (err) {
console.log('EdgeKV put error:', err);
}
return release.tag_name;
}
}
}
// 如果没找到beta版本,返回null (If no beta version found, return null)
return null;
}
/**
* 构建响应头 (Build response headers)
* @param {Headers} originalHeaders - 原始响应头 (Original response headers)
* @param {boolean} isDynamic - 是否为动态版本(latest/beta) (Whether it's a dynamic version)
* @param {string} githubUrl - GitHub URL
* @returns {Headers} 构建好的响应头 (Constructed response headers)
*/
function buildResponseHeaders(originalHeaders, isDynamic, githubUrl) {
const headers = new Headers(originalHeaders);
headers.set('X-GitHub-URL', githubUrl);
// 动态版本12小时缓存,静态版本永久缓存 (Dynamic versions: 12h cache, static versions: permanent cache)
if (isDynamic) {
headers.set('Cache-Control', 'public, max-age=43200');
} else {
headers.set('Cache-Control', 'public, max-age=31536000, immutable');
}
return headers;
}
/**
* 处理发布请求 (Handle release requests)
* @param {string} version - 版本标签,可以是具体版本(如v4.1.3-beta1)、latest或beta (Version tag, can be specific version, "latest", or "beta")
* @param {string} binaryFile - 二进制文件名 (Binary filename)
* @returns {Promise<Response>} 响应 (The response)
*/
async function handleRelease(version, binaryFile) {
// 构建GitHub URL (Build GitHub URL)
let githubUrl;
let isDynamic = false;
if (version === 'latest') {
// latest使用GitHub的latest重定向,动态版本 (latest uses GitHub's latest redirect, dynamic version)
githubUrl = `https://github.com/${GITHUB_REPO}/releases/latest/download/${binaryFile}`;
isDynamic = true;
} else if (version === 'beta') {
// 获取最新beta版本号,转换为具体版本 (Get latest beta version number, convert to specific version)
const betaVersion = await getLatestBetaVersion();
if (!betaVersion) {
return new Response('Beta release not found', { status: 404, headers: { 'Content-Type': 'text/plain' } });
}
// beta已转换为具体版本,按静态版本处理 (Beta converted to specific version, treat as static)
githubUrl = `https://github.com/${GITHUB_REPO}/releases/download/${betaVersion}/${binaryFile}`;
} else {
// 具体版本号,静态版本 (Specific version, static)
githubUrl = `https://github.com/${GITHUB_REPO}/releases/download/${version}/${binaryFile}`;
}
// 缓存key使用HTTP协议(ESA Cache引擎要求) (Cache key uses HTTP protocol, required by ESA Cache engine)
const cacheKey = githubUrl.replace('https://', 'http://');
// 尝试从缓存获取 (Try to get from cache first)
const cachedResponse = await cache.get(cacheKey);
if (cachedResponse) {
return cachedResponse;
}
// 从GitHub获取 (Fetch from GitHub)
const response = await fetch(githubUrl, { redirect: 'follow' });
if (!response.ok) {
return response;
}
// 构建响应头 (Build response headers)
const headers = buildResponseHeaders(response.headers, isDynamic, githubUrl);
// 创建最终响应 (Create final response)
const finalResponse = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: headers
});
// 先缓存再返回 (Cache first then return)
cache.put(cacheKey, finalResponse.clone()).catch(err => {
console.error('Cache error:', err);
});
return finalResponse;
}
/**
* 处理传入请求 (Handle incoming requests)
* @param {Request} request - 传入的请求 (The incoming request)
* @returns {Promise<Response>} 响应 (The response)
*/
async function handleRequest(request) {
const url = new URL(request.url);
const path = url.pathname;
// Handle redirects for old /doc/ URLs
// Redirect /index.en.html to /en/
if (path === '/index.en.html') {
return Response.redirect(url.origin + '/en/' + url.search + url.hash, 301);
}
// Redirect /doc/xxx.en.html to /en/xxx.html (must be before /doc/ redirect)
const enHtmlMatch = path.match(/^\/doc\/(.+)\.en\.html$/);
if (enHtmlMatch) {
const pagePath = enHtmlMatch[1];
return Response.redirect(url.origin + `/en/${pagePath}.html` + url.search + url.hash, 301);
}
// Redirect /doc/xxx to /xxx (301 permanent redirect)
if (path.startsWith('/doc/')) {
const newPath = path.replace('/doc/', '/');
return Response.redirect(url.origin + newPath + url.search + url.hash, 301);
}
// 解析请求路径 (Parse the request path)
// 格式: /releases/{version}/{binary_file}
// version 可以是 v4.1.3-beta1、latest 或 beta
const match = path.match(/^\/releases\/([\w.-]+)\/([\w.-]+)$/);
if (match) {
const [, version, binaryFile] = match;
return handleRelease(version, binaryFile);
}
// No match found - return 404 directly
return new Response('Not Found', {
status: 404,
headers: { 'Content-Type': 'text/plain' }
});
}
/**
* ES模块导出格式 (ES Module export format)
* 阿里云ESA要求导出包含fetch函数的对象 (Alibaba Cloud ESA requires exporting an object with a fetch function)
*/
export default {
async fetch(request, env, ctx) {
return handleRequest(request);
}
};