diff --git a/README.md b/README.md
index 5b41e0e..6ce4cf1 100644
--- a/README.md
+++ b/README.md
@@ -765,7 +765,7 @@ ALLOW_LAN_ACCESS=true LAN_ALLOWED_DEV_ORIGINS="kvideo.lan,192.168.50.10" npm run
- **ESLint 10**:代码质量检查
- **PostCSS 8**:CSS 处理器
-- **Vercel Analytics**:性能监控和分析
+- **Vercel Analytics**:仅在 Vercel 部署中启用的性能监控和分析
- **Cloudflare Pages**:边缘部署支持
### 架构特点
diff --git a/app-release.json b/app-release.json
index ad2e9ed..c69f9db 100644
--- a/app-release.json
+++ b/app-release.json
@@ -4,8 +4,18 @@
"name": "KVideo",
"branch": "main"
},
- "currentVersion": "4.9.4",
+ "currentVersion": "4.9.5",
"releases": [
+ {
+ "version": "4.9.5",
+ "publishedAt": "2026-07-09",
+ "title": "修复 Docker 登录页 Analytics 404",
+ "notes": [
+ "Vercel Analytics 现在只会在真实 Vercel 部署中注入,Docker、自托管和 Cloudflare Pages 不再请求 /_vercel/insights/script.js。",
+ "Cloudflare 部署环境判定优先于 Vercel 风格环境变量,避免适配器环境被误判为 Vercel。",
+ "新增部署环境单元测试,覆盖自托管、Vercel、Cloudflare 和混合环境变量场景。"
+ ]
+ },
{
"version": "4.9.4",
"publishedAt": "2026-07-08",
diff --git a/app/layout.tsx b/app/layout.tsx
index f72b002..68a1c38 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -16,6 +16,7 @@ import { ScrollPositionManager } from "@/components/ScrollPositionManager";
import { LocaleProvider } from "@/components/LocaleProvider";
import { RuntimeFeaturesProvider } from "@/components/RuntimeFeaturesProvider";
import { VideoTogetherController } from '@/components/VideoTogetherController';
+import { shouldEnableVercelAnalytics } from '@/lib/config/deployment';
import { getRuntimeFeatures } from "@/lib/server/runtime-features";
import { resolveSiteIconSrc } from '@/lib/server/site-icon';
import fs from 'fs';
@@ -86,6 +87,7 @@ export default async function RootLayout({
process.env.VIDEOTOGETHER_SCRIPT_URL?.trim() || DEFAULT_VIDEOTOGETHER_SCRIPT_URL;
const videoTogetherSettingUrl = process.env.VIDEOTOGETHER_SETTING_URL?.trim();
const videoTogetherEnvEnabled = process.env.VIDEOTOGETHER_ENABLED !== 'false';
+ const vercelAnalyticsEnabled = shouldEnableVercelAnalytics();
return (
@@ -136,7 +138,7 @@ export default async function RootLayout({
-
+ {vercelAnalyticsEnabled ? : null}
diff --git a/lib/config/deployment.ts b/lib/config/deployment.ts
new file mode 100644
index 0000000..38d2c21
--- /dev/null
+++ b/lib/config/deployment.ts
@@ -0,0 +1,19 @@
+type EnvLike = Record;
+
+export function isVercelDeployment(env: EnvLike = process.env): boolean {
+ return env.VERCEL === '1' || Boolean(env.VERCEL_ENV);
+}
+
+export function isCloudflareDeployment(env: EnvLike = process.env): boolean {
+ return (
+ env.CF_PAGES === '1' ||
+ Boolean(env.CF_PAGES_URL) ||
+ Boolean(env.CLOUDFLARE_ACCOUNT_ID) ||
+ Boolean(env.CF_ACCOUNT_ID) ||
+ Boolean(env.WORKERS_CI)
+ );
+}
+
+export function shouldEnableVercelAnalytics(env: EnvLike = process.env): boolean {
+ return isVercelDeployment(env) && !isCloudflareDeployment(env);
+}
diff --git a/lib/server/runtime-features.ts b/lib/server/runtime-features.ts
index b0cc56b..46968ea 100644
--- a/lib/server/runtime-features.ts
+++ b/lib/server/runtime-features.ts
@@ -1,20 +1,7 @@
import 'server-only';
import type { RuntimeFeatures } from '@/lib/config/runtime-features';
-
-function isVercelDeployment(): boolean {
- return process.env.VERCEL === '1' || Boolean(process.env.VERCEL_ENV);
-}
-
-function isCloudflareDeployment(): boolean {
- return (
- process.env.CF_PAGES === '1' ||
- Boolean(process.env.CF_PAGES_URL) ||
- Boolean(process.env.CLOUDFLARE_ACCOUNT_ID) ||
- Boolean(process.env.CF_ACCOUNT_ID) ||
- Boolean(process.env.WORKERS_CI)
- );
-}
+import { isCloudflareDeployment, isVercelDeployment } from '@/lib/config/deployment';
function getRestrictedFeatures(
deploymentProvider: RuntimeFeatures['deploymentProvider'],
@@ -31,14 +18,14 @@ function getRestrictedFeatures(
}
export function getRuntimeFeatures(): RuntimeFeatures {
- if (isVercelDeployment()) {
- return getRestrictedFeatures('vercel', 'Vercel');
- }
-
if (isCloudflareDeployment()) {
return getRestrictedFeatures('cloudflare', 'Cloudflare');
}
+ if (isVercelDeployment()) {
+ return getRestrictedFeatures('vercel', 'Vercel');
+ }
+
return {
deploymentProvider: 'self-hosted',
deploymentProviderLabel: '自托管',
@@ -48,4 +35,3 @@ export function getRuntimeFeatures(): RuntimeFeatures {
restrictionSummary: null,
};
}
-
diff --git a/package-lock.json b/package-lock.json
index 3cdac9f..1e031f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "kvideo",
- "version": "4.9.4",
+ "version": "4.9.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "kvideo",
- "version": "4.9.4",
+ "version": "4.9.5",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
diff --git a/package.json b/package.json
index 966330c..f701178 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "kvideo",
- "version": "4.9.4",
+ "version": "4.9.5",
"private": true,
"scripts": {
"dev": "node scripts/next-with-lan-access.mjs dev",
diff --git a/tests/deployment.test.ts b/tests/deployment.test.ts
new file mode 100644
index 0000000..3106565
--- /dev/null
+++ b/tests/deployment.test.ts
@@ -0,0 +1,38 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+
+import {
+ isCloudflareDeployment,
+ isVercelDeployment,
+ shouldEnableVercelAnalytics,
+} from '@/lib/config/deployment';
+
+test('self-hosted deployments do not enable Vercel Analytics', () => {
+ const env = {};
+
+ assert.equal(isVercelDeployment(env), false);
+ assert.equal(isCloudflareDeployment(env), false);
+ assert.equal(shouldEnableVercelAnalytics(env), false);
+});
+
+test('Vercel deployments enable Vercel Analytics', () => {
+ assert.equal(shouldEnableVercelAnalytics({ VERCEL: '1' }), true);
+ assert.equal(shouldEnableVercelAnalytics({ VERCEL_ENV: 'production' }), true);
+});
+
+test('Cloudflare deployments do not enable Vercel Analytics', () => {
+ assert.equal(shouldEnableVercelAnalytics({ CF_PAGES: '1' }), false);
+ assert.equal(shouldEnableVercelAnalytics({ CF_PAGES_URL: 'https://kvideo.pages.dev' }), false);
+});
+
+test('Cloudflare wins when adapter environments expose Vercel-like variables', () => {
+ const env = {
+ VERCEL: '1',
+ VERCEL_ENV: 'production',
+ CF_PAGES: '1',
+ };
+
+ assert.equal(isVercelDeployment(env), true);
+ assert.equal(isCloudflareDeployment(env), true);
+ assert.equal(shouldEnableVercelAnalytics(env), false);
+});