mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-01 23:15:27 +08:00
feat: Add Vercel deployment protection bypass support, enhance related error handling, and update documentation.
This commit is contained in:
@@ -77,3 +77,9 @@ DS2API_ADMIN_KEY=admin
|
||||
# VERCEL_TOKEN=your-vercel-token
|
||||
# VERCEL_PROJECT_ID=prj_xxxxxxxxxxxx
|
||||
# VERCEL_TEAM_ID=team_xxxxxxxxxxxx
|
||||
|
||||
# Optional: Vercel deployment protection bypass secret.
|
||||
# If deployment protection is enabled, DS2API will use this value as
|
||||
# x-vercel-protection-bypass for internal Node->Go calls on Vercel.
|
||||
# You can also use VERCEL_AUTOMATION_BYPASS_SECRET directly.
|
||||
# DS2API_VERCEL_PROTECTION_BYPASS=your-bypass-secret
|
||||
|
||||
@@ -152,6 +152,12 @@ Vercel is validating frontend output against `public`. This repo builds WebUI in
|
||||
|
||||
If you manually changed Output Directory in Project Settings, set it to `static` (or clear it and let repo config apply).
|
||||
|
||||
If API responses return Vercel HTML `Authentication Required` (instead of JSON), the request is blocked by Vercel Deployment Protection:
|
||||
|
||||
- Disable protection for that deployment/environment (recommended for public API use)
|
||||
- Or send `x-vercel-protection-bypass` in requests
|
||||
- If only internal Node->Go calls are blocked, set `VERCEL_AUTOMATION_BYPASS_SECRET` (or `DS2API_VERCEL_PROTECTION_BYPASS`)
|
||||
|
||||
Vercel streaming note (important):
|
||||
|
||||
- Vercel Go Runtime applies platform-level buffering, so this repo uses a hybrid path on Vercel (`Go prepare + Node stream`) to restore real-time SSE behavior.
|
||||
|
||||
@@ -150,6 +150,12 @@ No Output Directory named "public" found after the Build completed.
|
||||
|
||||
若你在项目设置里手动改过 Output Directory,请同步改为 `static` 或清空让仓库配置生效。
|
||||
|
||||
若接口返回 Vercel 的 HTML 页面 `Authentication Required`(而不是 JSON),说明被 Vercel Deployment Protection 拦截:
|
||||
|
||||
- 关闭该部署/环境的 Protection(推荐用于公开 API)
|
||||
- 或给请求加 `x-vercel-protection-bypass`
|
||||
- 若仅是 Vercel 内部 Node->Go 调用被拦截,可设置 `VERCEL_AUTOMATION_BYPASS_SECRET`(或 `DS2API_VERCEL_PROTECTION_BYPASS`)
|
||||
|
||||
Vercel 流式说明(重要):
|
||||
|
||||
- Vercel 的 Go Runtime 存在平台层响应缓冲,因此本项目在 Vercel 上采用“Go prepare + Node stream”的混合链路来恢复实时 SSE。
|
||||
|
||||
@@ -205,7 +205,10 @@ function setCorsHeaders(res) {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, DELETE');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, X-Ds2-Target-Account');
|
||||
res.setHeader(
|
||||
'Access-Control-Allow-Headers',
|
||||
'Content-Type, Authorization, X-API-Key, X-Ds2-Target-Account, X-Vercel-Protection-Bypass',
|
||||
);
|
||||
}
|
||||
|
||||
function header(req, key) {
|
||||
@@ -238,6 +241,10 @@ async function fetchStreamPrepare(req, rawBody) {
|
||||
const url = new URL(`${proto}://${host}${req.url || '/v1/chat/completions'}`);
|
||||
url.searchParams.set('__go', '1');
|
||||
url.searchParams.set('__stream_prepare', '1');
|
||||
const protectionBypass = resolveProtectionBypass(req);
|
||||
if (protectionBypass) {
|
||||
url.searchParams.set('x-vercel-protection-bypass', protectionBypass);
|
||||
}
|
||||
|
||||
const upstream = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
@@ -246,6 +253,7 @@ async function fetchStreamPrepare(req, rawBody) {
|
||||
'x-api-key': asString(header(req, 'x-api-key')),
|
||||
'x-ds2-target-account': asString(header(req, 'x-ds2-target-account')),
|
||||
'x-ds2-internal-token': internalSecret(),
|
||||
'x-vercel-protection-bypass': protectionBypass,
|
||||
'content-type': asString(header(req, 'content-type')) || 'application/json',
|
||||
},
|
||||
body: rawBody,
|
||||
@@ -269,6 +277,14 @@ async function fetchStreamPrepare(req, rawBody) {
|
||||
}
|
||||
|
||||
function relayPreparedFailure(res, prep) {
|
||||
if (prep.status === 401 && looksLikeVercelAuthPage(prep.text)) {
|
||||
writeOpenAIError(
|
||||
res,
|
||||
401,
|
||||
'Vercel Deployment Protection blocked internal prepare request. Disable protection for this deployment or set VERCEL_AUTOMATION_BYPASS_SECRET.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
res.statusCode = prep.status || 500;
|
||||
res.setHeader('Content-Type', prep.contentType || 'application/json');
|
||||
if (prep.text) {
|
||||
@@ -294,6 +310,22 @@ function internalSecret() {
|
||||
return asString(process.env.DS2API_VERCEL_INTERNAL_SECRET) || asString(process.env.DS2API_ADMIN_KEY) || 'admin';
|
||||
}
|
||||
|
||||
function resolveProtectionBypass(req) {
|
||||
const fromHeader = asString(header(req, 'x-vercel-protection-bypass'));
|
||||
if (fromHeader) {
|
||||
return fromHeader;
|
||||
}
|
||||
return asString(process.env.VERCEL_AUTOMATION_BYPASS_SECRET) || asString(process.env.DS2API_VERCEL_PROTECTION_BYPASS);
|
||||
}
|
||||
|
||||
function looksLikeVercelAuthPage(text) {
|
||||
const body = asString(text).toLowerCase();
|
||||
if (!body) {
|
||||
return false;
|
||||
}
|
||||
return body.includes('authentication required') && body.includes('vercel');
|
||||
}
|
||||
|
||||
function parseChunkForContent(chunk, thinkingEnabled, currentType) {
|
||||
if (!chunk || typeof chunk !== 'object' || !Object.prototype.hasOwnProperty.call(chunk, 'v')) {
|
||||
return { parts: [], finished: false, newType: currentType };
|
||||
@@ -472,6 +504,10 @@ async function proxyToGo(req, res, rawBody) {
|
||||
const host = asString(header(req, 'host'));
|
||||
const url = new URL(`${proto}://${host}${req.url || '/v1/chat/completions'}`);
|
||||
url.searchParams.set('__go', '1');
|
||||
const protectionBypass = resolveProtectionBypass(req);
|
||||
if (protectionBypass) {
|
||||
url.searchParams.set('x-vercel-protection-bypass', protectionBypass);
|
||||
}
|
||||
|
||||
const upstream = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
@@ -479,6 +515,7 @@ async function proxyToGo(req, res, rawBody) {
|
||||
authorization: asString(header(req, 'authorization')),
|
||||
'x-api-key': asString(header(req, 'x-api-key')),
|
||||
'x-ds2-target-account': asString(header(req, 'x-ds2-target-account')),
|
||||
'x-vercel-protection-bypass': protectionBypass,
|
||||
'content-type': asString(header(req, 'content-type')) || 'application/json',
|
||||
},
|
||||
body: rawBody,
|
||||
|
||||
Reference in New Issue
Block a user