diff --git a/CHANGELOG.md b/CHANGELOG.md
index 721a128..b8f5e5b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## 4.9.8 - 2026-07-09
+
+- 修复登录完成跳转后 Network 面板出现 `/api/user/config` 和 `/api/user/sync` 两个 400 请求的问题。
+- `AutoSync` 改为在 `PasswordGate` 确认服务端 session 并放行页面内容后再挂载,避免本地旧 session 早于服务端 cookie 校验触发云同步。
+- 用户配置和云同步 API 在未认证时返回 `401 Authentication required`,不再使用 `400 Missing profileId` 表达会话缺失。
+- 新增未认证响应回归测试,锁定受保护同步接口的状态码语义。
+
## 4.9.7 - 2026-07-09
- 修复 Android TV / 小米电视旧 WebView 83 打开站点后白屏的问题。
diff --git a/app-release.json b/app-release.json
index 7d8d7ba..06a155a 100644
--- a/app-release.json
+++ b/app-release.json
@@ -4,8 +4,18 @@
"name": "KVideo",
"branch": "main"
},
- "currentVersion": "4.9.7",
+ "currentVersion": "4.9.8",
"releases": [
+ {
+ "version": "4.9.8",
+ "publishedAt": "2026-07-09",
+ "title": "修复登录后同步接口 400",
+ "notes": [
+ "自动同步组件现在只会在访问密码 gate 确认服务端会话并完成本地 session 镜像后挂载,避免登录跳转阶段提前请求 /api/user/config 与 /api/user/sync。",
+ "用户配置和云同步 API 在没有有效服务端 session 时改为返回 401 Authentication required,不再把未认证状态伪装成 Missing profileId 的 400。",
+ "新增未认证响应回归测试,防止受保护同步接口的状态码语义再次退化。"
+ ]
+ },
{
"version": "4.9.7",
"publishedAt": "2026-07-09",
diff --git a/app/api/user/config/route.ts b/app/api/user/config/route.ts
index bc0f4b4..3195d41 100644
--- a/app/api/user/config/route.ts
+++ b/app/api/user/config/route.ts
@@ -7,6 +7,7 @@
import { Redis } from '@upstash/redis';
import { NextRequest, NextResponse } from 'next/server';
+import { authenticationRequiredResponse } from '@/lib/server/api-responses';
import { getServerSession } from '@/lib/server/auth';
export const runtime = 'edge';
@@ -23,7 +24,7 @@ export async function GET(request: NextRequest) {
const profileId = session?.profileId;
if (!profileId) {
- return NextResponse.json({ error: 'Missing profileId' }, { status: 400 });
+ return authenticationRequiredResponse();
}
try {
@@ -43,7 +44,7 @@ export async function POST(request: NextRequest) {
const profileId = session?.profileId;
if (!profileId) {
- return NextResponse.json({ error: 'Missing profileId' }, { status: 400 });
+ return authenticationRequiredResponse();
}
try {
diff --git a/app/api/user/sync/route.ts b/app/api/user/sync/route.ts
index 716ea1f..cd190d6 100644
--- a/app/api/user/sync/route.ts
+++ b/app/api/user/sync/route.ts
@@ -1,5 +1,6 @@
import { Redis } from '@upstash/redis';
import { NextRequest, NextResponse } from 'next/server';
+import { authenticationRequiredResponse } from '@/lib/server/api-responses';
import { getServerSession } from '@/lib/server/auth';
// 确保这行代码在整个文件中只出现一次
@@ -12,7 +13,7 @@ export async function GET(request: NextRequest) {
const profileId = session?.profileId;
if (!profileId) {
- return NextResponse.json({ error: 'Missing profileId' }, { status: 400 });
+ return authenticationRequiredResponse();
}
try {
@@ -32,7 +33,7 @@ export async function POST(request: NextRequest) {
const profileId = session?.profileId;
if (!profileId) {
- return NextResponse.json({ error: 'Missing profileId' }, { status: 400 });
+ return authenticationRequiredResponse();
}
try {
diff --git a/app/layout.tsx b/app/layout.tsx
index 68a1c38..565b6bf 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -116,8 +116,6 @@ export default async function RootLayout({
scriptUrl={videoTogetherScriptUrl}
settingUrl={videoTogetherSettingUrl}
/>
- {/* 加入自动同步组件,它会在后台默默工作,我们放在 ThemeProvider 内部的最前面 */}
-
@@ -132,6 +130,7 @@ export default async function RootLayout({
process.env.UPSTASH_REDIS_REST_TOKEN
)
)}>
+
{children}
diff --git a/components/AutoSync.tsx b/components/AutoSync.tsx
index e2bb6e0..b56659f 100644
--- a/components/AutoSync.tsx
+++ b/components/AutoSync.tsx
@@ -8,10 +8,10 @@ import { useConfigSync } from '@/lib/hooks/useConfigSync';
import { getSession } from '@/lib/store/auth-store';
// 防抖函数,防止频繁请求
-function debounce(fn: Function, delay: number) {
- let timeoutId: NodeJS.Timeout;
- return (...args: any[]) => {
- clearTimeout(timeoutId);
+function debounce(fn: (...args: Args) => void, delay: number) {
+ let timeoutId: ReturnType | undefined;
+ return (...args: Args) => {
+ if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
diff --git a/lib/server/api-responses.ts b/lib/server/api-responses.ts
new file mode 100644
index 0000000..81f55aa
--- /dev/null
+++ b/lib/server/api-responses.ts
@@ -0,0 +1,5 @@
+import { NextResponse } from 'next/server';
+
+export function authenticationRequiredResponse(): NextResponse {
+ return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
+}
diff --git a/package-lock.json b/package-lock.json
index 5156995..0d7f834 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "kvideo",
- "version": "4.9.7",
+ "version": "4.9.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "kvideo",
- "version": "4.9.7",
+ "version": "4.9.8",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
diff --git a/package.json b/package.json
index 31b16eb..bf04421 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "kvideo",
- "version": "4.9.7",
+ "version": "4.9.8",
"private": true,
"scripts": {
"dev": "node scripts/next-with-lan-access.mjs dev",
diff --git a/tests/api-responses.test.ts b/tests/api-responses.test.ts
new file mode 100644
index 0000000..e794aaa
--- /dev/null
+++ b/tests/api-responses.test.ts
@@ -0,0 +1,12 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+
+import { authenticationRequiredResponse } from '@/lib/server/api-responses';
+
+test('authenticationRequiredResponse returns a 401 JSON response', async () => {
+ const response = authenticationRequiredResponse();
+
+ assert.equal(response.status, 401);
+ assert.equal(response.headers.get('content-type')?.includes('application/json'), true);
+ assert.deepEqual(await response.json(), { error: 'Authentication required' });
+});