Merge PR #421: feat: add Turso (libSQL) storage support for EdgeOne deployment

This commit is contained in:
mtvpls
2026-07-12 16:41:11 +08:00
21 changed files with 980 additions and 40 deletions
+199
View File
@@ -24,6 +24,7 @@ import { CSS } from '@dnd-kit/utilities';
import {
AlertCircle,
AlertTriangle,
BarChart3,
BookMarked,
BookOpen,
Bot,
@@ -406,6 +407,11 @@ interface SiteConfig {
OIDCClientId?: string;
OIDCClientSecret?: string;
OIDCButtonText?: string;
AnalyticsEnabled?: boolean;
AnalyticsProvider?: 'umami' | 'google' | 'clarity' | 'custom';
AnalyticsScriptUrl?: string;
AnalyticsWebsiteId?: string;
AnalyticsCustomScript?: string;
}
// 视频源数据类型
@@ -10198,6 +10204,11 @@ const SiteConfigComponent = ({
OIDCClientId: '',
OIDCClientSecret: '',
OIDCButtonText: '',
AnalyticsEnabled: false,
AnalyticsProvider: 'umami',
AnalyticsScriptUrl: '',
AnalyticsWebsiteId: '',
AnalyticsCustomScript: '',
});
// 豆瓣数据源相关状态
@@ -11528,6 +11539,194 @@ const SiteConfigComponent = ({
</div>
</details>
{/* 流量统计配置 */}
<details className='group rounded-lg border border-gray-200 p-4 dark:border-gray-700'>
<summary className='flex cursor-pointer items-center justify-between font-medium text-gray-900 dark:text-gray-100'>
<span className='flex items-center gap-2'>
<BarChart3 className='h-5 w-5' />
</span>
<ChevronDown className='h-5 w-5 transition-transform group-open:rotate-180' />
</summary>
<div className='mt-4 space-y-4'>
{/* 启用开关 */}
<div className='flex items-center justify-between'>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300'>
</label>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
UmamiGoogle Analytics
</p>
</div>
<button
type='button'
onClick={() =>
setSiteSettings((prev) => ({
...prev,
AnalyticsEnabled: !prev.AnalyticsEnabled,
}))
}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
siteSettings.AnalyticsEnabled
? buttonStyles.toggleOn
: buttonStyles.toggleOff
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full ${
buttonStyles.toggleThumb
} transition-transform ${
siteSettings.AnalyticsEnabled
? buttonStyles.toggleThumbOn
: buttonStyles.toggleThumbOff
}`}
/>
</button>
</div>
{siteSettings.AnalyticsEnabled && (
<>
{/* 统计服务提供商 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300'>
</label>
<select
value={siteSettings.AnalyticsProvider}
onChange={(e) =>
setSiteSettings((prev) => ({
...prev,
AnalyticsProvider: e.target.value as 'umami' | 'google' | 'clarity' | 'custom',
}))
}
className='mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200'
>
<option value='umami'>Umami</option>
<option value='google'>Google Analytics</option>
<option value='clarity'>Microsoft Clarity+</option>
<option value='custom'></option>
</select>
</div>
{siteSettings.AnalyticsProvider === 'umami' && (
<>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300'>
Umami
</label>
<input
type='text'
value={siteSettings.AnalyticsScriptUrl}
onChange={(e) =>
setSiteSettings((prev) => ({
...prev,
AnalyticsScriptUrl: e.target.value,
}))
}
placeholder='https://your-umami-server.com/script.js'
className='mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200'
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
Umami script.js URL
</p>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300'>
ID (Website ID)
</label>
<input
type='text'
value={siteSettings.AnalyticsWebsiteId}
onChange={(e) =>
setSiteSettings((prev) => ({
...prev,
AnalyticsWebsiteId: e.target.value,
}))
}
placeholder='e.g. 12345678-abcd-efgh-ijkl-1234567890ab'
className='mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200'
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
Umami Website ID
</p>
</div>
</>
)}
{siteSettings.AnalyticsProvider === 'google' && (
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300'>
Measurement ID
</label>
<input
type='text'
value={siteSettings.AnalyticsWebsiteId}
onChange={(e) =>
setSiteSettings((prev) => ({
...prev,
AnalyticsWebsiteId: e.target.value,
}))
}
placeholder='G-XXXXXXXXXX'
className='mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200'
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
Google Analytics 4 Measurement ID GA
</p>
</div>
)}
{siteSettings.AnalyticsProvider === 'clarity' && (
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300'>
Project ID
</label>
<input
type='text'
value={siteSettings.AnalyticsWebsiteId}
onChange={(e) =>
setSiteSettings((prev) => ({
...prev,
AnalyticsWebsiteId: e.target.value,
}))
}
placeholder='e.g. abc1234567'
className='mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200'
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
Microsoft Clarity Project ID clarity.microsoft.com
</p>
</div>
)}
{siteSettings.AnalyticsProvider === 'custom' && (
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300'>
</label>
<textarea
value={siteSettings.AnalyticsCustomScript}
onChange={(e) =>
setSiteSettings((prev) => ({
...prev,
AnalyticsCustomScript: e.target.value,
}))
}
placeholder='粘贴完整的统计脚本代码,如百度统计、Plausible、51la 等...'
rows={6}
className='mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200'
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
&lt;head&gt;
</p>
</div>
)}
</>
)}
</div>
</details>
{/* 操作按钮 */}
<div className='flex justify-end'>
<button
@@ -249,8 +249,8 @@ async function getUserPasswordV2(username: string): Promise<string | null> {
return null;
}
// D1 存储:使用 getUserPasswordHash 方法
if (storageType === 'd1') {
// D1/Turso 存储:使用 getUserPasswordHash 方法
if (storageType === 'd1' || storageType === 'turso') {
if (typeof storage.getUserPasswordHash === 'function') {
return await storage.getUserPasswordHash(username);
}
@@ -180,8 +180,8 @@ export async function POST(req: NextRequest) {
const createdAt = userV2?.created_at || Date.now();
// 根据存储类型使用不同的导入方法
if (storageType === 'd1') {
// D1 存储:使用 createUserWithHashedPassword 方法
if (storageType === 'd1' || storageType === 'turso') {
// D1/Turso 存储:使用 createUserWithHashedPassword 方法
if (typeof storage.createUserWithHashedPassword === 'function') {
await storage.createUserWithHashedPassword(
username,
@@ -193,9 +193,9 @@ export async function POST(req: NextRequest) {
userV2?.enabledApis,
userV2?.banned
);
console.log(`用户 ${username} 导入成功 (D1)`);
console.log(`用户 ${username} 导入成功 (${storageType})`);
} else {
console.error(`D1 storage 缺少 createUserWithHashedPassword 方法`);
console.error(`${storageType} storage 缺少 createUserWithHashedPassword 方法`);
return false;
}
} else if (storageType === 'postgres') {
+25 -1
View File
@@ -83,6 +83,11 @@ export async function POST(request: NextRequest) {
OIDCClientSecret,
OIDCButtonText,
OIDCMinTrustLevel,
AnalyticsEnabled,
AnalyticsProvider,
AnalyticsScriptUrl,
AnalyticsWebsiteId,
AnalyticsCustomScript,
} = body as {
SiteName: string;
Announcement: string;
@@ -138,6 +143,11 @@ export async function POST(request: NextRequest) {
OIDCClientSecret?: string;
OIDCButtonText?: string;
OIDCMinTrustLevel?: number;
AnalyticsEnabled?: boolean;
AnalyticsProvider?: 'umami' | 'google' | 'clarity' | 'custom';
AnalyticsScriptUrl?: string;
AnalyticsWebsiteId?: string;
AnalyticsCustomScript?: string;
};
// 参数校验
@@ -224,7 +234,16 @@ export async function POST(request: NextRequest) {
(OIDCClientSecret !== undefined &&
typeof OIDCClientSecret !== 'string') ||
(OIDCButtonText !== undefined && typeof OIDCButtonText !== 'string') ||
(OIDCMinTrustLevel !== undefined && typeof OIDCMinTrustLevel !== 'number')
(OIDCMinTrustLevel !== undefined && typeof OIDCMinTrustLevel !== 'number') ||
(AnalyticsEnabled !== undefined && typeof AnalyticsEnabled !== 'boolean') ||
(AnalyticsProvider !== undefined &&
AnalyticsProvider !== 'umami' &&
AnalyticsProvider !== 'google' &&
AnalyticsProvider !== 'clarity' &&
AnalyticsProvider !== 'custom') ||
(AnalyticsScriptUrl !== undefined && typeof AnalyticsScriptUrl !== 'string') ||
(AnalyticsWebsiteId !== undefined && typeof AnalyticsWebsiteId !== 'string') ||
(AnalyticsCustomScript !== undefined && typeof AnalyticsCustomScript !== 'string')
) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
@@ -295,6 +314,11 @@ export async function POST(request: NextRequest) {
OIDCClientSecret,
OIDCButtonText,
OIDCMinTrustLevel,
AnalyticsEnabled,
AnalyticsProvider,
AnalyticsScriptUrl,
AnalyticsWebsiteId,
AnalyticsCustomScript,
};
// 写入数据库
+1
View File
@@ -244,6 +244,7 @@ function DuanjuPageClient() {
year={item.year}
from='source-search'
type='tv'
isDuanju
cmsData={{
desc: item.desc,
episodes: item.episodes,
+49
View File
@@ -115,6 +115,11 @@ export default async function RootLayout({
let liveEnabled = true;
let webLiveEnabled = false;
let customAdFilterVersion = 0;
let analyticsEnabled = false;
let analyticsProvider: 'umami' | 'google' | 'clarity' | 'custom' = 'umami';
let analyticsScriptUrl = '';
let analyticsWebsiteId = '';
let analyticsCustomScript = '';
let musicFeatureEnabled = false;
let suwayomiEnabled = false;
let booksEnabled =
@@ -202,6 +207,12 @@ export default async function RootLayout({
webLiveEnabled = config.WebLiveEnabled ?? false;
// 自定义去广告代码版本号
customAdFilterVersion = config.SiteConfig?.CustomAdFilterVersion || 0;
// 流量统计配置
analyticsEnabled = config.SiteConfig?.AnalyticsEnabled || false;
analyticsProvider = config.SiteConfig?.AnalyticsProvider || 'umami';
analyticsScriptUrl = config.SiteConfig?.AnalyticsScriptUrl || '';
analyticsWebsiteId = config.SiteConfig?.AnalyticsWebsiteId || '';
analyticsCustomScript = config.SiteConfig?.AnalyticsCustomScript || '';
// 音乐功能配置
musicFeatureEnabled = config.MusicConfig?.Enabled || false;
musicProxyEnabled = config.MusicConfig?.ProxyEnabled ?? true;
@@ -335,6 +346,44 @@ export default async function RootLayout({
__html: `window.RUNTIME_CONFIG = ${JSON.stringify(runtimeConfig)};`,
}}
/>
{/* 流量统计脚本 */}
{analyticsEnabled && analyticsProvider === 'umami' && analyticsScriptUrl && (
<>
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script
async
defer
data-website-id={analyticsWebsiteId}
src={analyticsScriptUrl}
/>
</>
)}
{analyticsEnabled && analyticsProvider === 'google' && analyticsWebsiteId && (
<>
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${analyticsWebsiteId}`}
/>
<script
dangerouslySetInnerHTML={{
__html: `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', '${analyticsWebsiteId}');`,
}}
/>
</>
)}
{analyticsEnabled && analyticsProvider === 'clarity' && analyticsWebsiteId && (
<script
dangerouslySetInnerHTML={{
__html: `(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);})(window,document,"clarity","script","${analyticsWebsiteId}");`,
}}
/>
)}
{analyticsEnabled && analyticsProvider === 'custom' && analyticsCustomScript && (
<script
dangerouslySetInnerHTML={{ __html: analyticsCustomScript }}
/>
)}
</head>
<body
className={`${inter.className} min-h-screen bg-white text-gray-900 dark:bg-black dark:text-gray-200`}
+6
View File
@@ -8736,6 +8736,12 @@ function PlayPageClient() {
// 条件:当前播放时间 < 10秒 且 播放记录时间 > 10秒
const checkPlayRecordJump = async () => {
try {
// 短剧不显示"上次播放到"提示(短剧单集太短,提示意义不大)
if (searchParams.get('duanju') === '1') {
playRecordJumpInitialCheckRef.current = false;
return;
}
// 仅在进入播放后的首次检查时处理,避免本次会话新生成的记录触发恢复按钮
if (!playRecordJumpInitialCheckRef.current) {
return;