Skip to content

Commit bc9eaea

Browse files
authored
Merge pull request #274 from InvolutionHell/backend
Backend
2 parents 08d4d26 + d263499 commit bc9eaea

34 files changed

Lines changed: 5838 additions & 4549 deletions

.env.sample

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
# 本文件提供运行本项目所需的环境变量示例。
22
# 提交代码时请提交本文件而不是实际的 .env,真实密钥请存放在个人或 CI 配置中。
33

4-
# NextAuth 基本配置
5-
AUTH_URL=http://localhost:3000 #https://involutionhell.com
6-
# 生成 32 字节以上的随机字符串,可用 openssl: `openssl rand -base64 32`
7-
AUTH_SECRET=
8-
# GitHub OAuth App 的 Client ID / Secret,可在 GitHub Developer settings 中创建
4+
# 后端地址(Spring Boot,认证已迁移到后端,NextAuth 已移除)
5+
# 服务端 API Route 调用后端时使用(不暴露给浏览器)
6+
BACKEND_URL=http://localhost:8080
7+
# 客户端组件(如登录按钮)直接跳转后端时使用
8+
NEXT_PUBLIC_BACKEND_URL=http://localhost:8080
9+
10+
# GitHub OAuth 配置 (由后端处理,此处仅作为示例配置参考)
911
AUTH_GITHUB_ID=
1012
AUTH_GITHUB_SECRET=
13+
AUTH_SECRET=
14+
AUTH_GITHUB_ID_DEV=
15+
AUTH_GITHUB_SECRET_DEV=
16+
AUTH_TRUST_HOST=true
17+
AUTH_URL=http://localhost:3000
18+
1119
# 可选:用于访问 GitHub API(例如同步仓库)
1220
GITHUB_TOKEN=
1321

@@ -16,6 +24,10 @@ INDEXNOW_API_TOKEN=
1624
#Open的Key
1725
INDEXNOW_KEY=5b6ef14a7406496b8a2ce8ab17820b34
1826
NEXT_PUBLIC_SITE_URL=https://involutionhell.com
27+
# 内部识别/认证 Key
28+
INTERN_KEY=
29+
# Neon 项目 ID
30+
NEON_PROJECT_ID=
1931
# Neon 提供的 Postgres 连接。
2032
# 登录 Neon 控制台 → 数据库 → "Connect" → "Connection details",可以复制以下所有变量。
2133
# 推荐连接字符串
@@ -44,6 +56,8 @@ POSTGRES_PRISMA_URL=
4456
NEXT_PUBLIC_STACK_PROJECT_ID=
4557
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=
4658
STACK_SECRET_SERVER_KEY=
59+
# Vercel OIDC Token
60+
VERCEL_OIDC_TOKEN=
4761

4862
# R2的存储桶,用于提供图片自动上传服务
4963
R2_ACCOUNT_ID=?

.github/workflows/content-check.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ jobs:
3939

4040
- uses: actions/setup-node@v4
4141
with:
42-
node-version: 20
42+
node-version: 22
4343
cache: "pnpm"
4444

4545
# Verify pnpm version matches package.json packageManager field
4646
- name: Check pnpm version
4747
run: node scripts/check-pnpm-version.mjs
4848

4949
- run: pnpm install --frozen-lockfile
50-
50+
5151
# Verify lockfile wasn't modified by install
5252
- name: Check lockfile consistency
5353
run: |
@@ -66,7 +66,7 @@ jobs:
6666
exit 1
6767
fi
6868
echo "✅ Lockfile is consistent"
69-
69+
7070
- name: Run tests
7171
run: pnpm test
7272
# Non-blocking image migration + lint (visibility only)

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828

2929
- uses: actions/setup-node@v4
3030
with:
31-
node-version: 20
31+
node-version: 22
3232
cache: pnpm
3333

3434
# Verify pnpm version matches package.json packageManager field

.github/workflows/sync-uuid.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444

4545
- uses: actions/setup-node@v4
4646
with:
47-
node-version: 20
47+
node-version: 22
4848
cache: "pnpm" # 顺便启用 pnpm 缓存,加速
4949

5050
# Verify pnpm version matches package.json packageManager field

.node-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
22

app/api/analytics/route.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { prisma } from "@/lib/db";
2+
import { resolveUserId } from "@/lib/server-auth";
23

34
export async function POST(req: Request) {
45
try {
5-
const { eventType, eventData, userId } = await req.json();
6+
const { eventType, eventData } = await req.json();
67

78
if (!eventType) {
89
return Response.json(
@@ -11,11 +12,15 @@ export async function POST(req: Request) {
1112
);
1213
}
1314

15+
// 服务端验证身份,不信任客户端传入的 userId
16+
const userId = await resolveUserId(req);
17+
1418
await prisma.analyticsEvent.create({
1519
data: {
1620
eventType,
1721
eventData: eventData ?? {},
18-
userId: userId ? parseInt(String(userId)) : null,
22+
// userId 对应 user_accounts.id(BigInt);匿名访问为 null
23+
...(userId != null && { userId }),
1924
},
2025
});
2126

app/api/auth/[...nextauth]/route.ts

Lines changed: 0 additions & 2 deletions
This file was deleted.

app/api/chat/route.ts

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,65 @@ interface ChatRequest {
2626
chatId?: string;
2727
}
2828

29+
import { resolveUserId } from "@/lib/server-auth";
30+
2931
export async function POST(req: Request) {
32+
// 1. 克隆请求,因为如果代理失败,后面的代码还需要读取 req.json()
33+
const proxyReq = req.clone();
34+
35+
// ====== 尝试优雅降级代理到 Java 后端 ======
36+
try {
37+
const backendUrl = process.env.BACKEND_URL;
38+
if (!backendUrl) throw new Error("BACKEND_URL is not configured.");
39+
const controller = new AbortController();
40+
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
41+
42+
// 原封不动把前端的参数丢给 Java
43+
let proxyRes: Response;
44+
try {
45+
proxyRes = await fetch(`${backendUrl}/openai/responses/stream`, {
46+
method: "POST",
47+
headers: {
48+
"Content-Type": "application/json",
49+
// 浏览器侧用 x-satoken 传递 token,转发给后端时改回后端期望的 satoken
50+
...(req.headers.get("x-satoken")
51+
? { satoken: req.headers.get("x-satoken")! }
52+
: {}),
53+
},
54+
body: await proxyReq.text(),
55+
signal: controller.signal,
56+
});
57+
} finally {
58+
// 无论成功还是抛出(网络错误/超时中断),都清除定时器
59+
clearTimeout(timeoutId);
60+
}
61+
62+
// 如果 Java 后端返回成功,则直接把它的流传回浏览器,提前结束
63+
if (proxyRes.ok && proxyRes.body) {
64+
console.log(
65+
"[Chat Fallback Proxy] 🚀 Java Backend responded successfully. Piping stream...",
66+
);
67+
return new Response(proxyRes.body, {
68+
headers: {
69+
"Content-Type":
70+
proxyRes.headers.get("Content-Type") || "text/plain; charset=utf-8",
71+
},
72+
});
73+
} else {
74+
console.warn(
75+
`[Chat Fallback Proxy] ⚠️ Java Backend returned status: ${proxyRes.status}, fallback to local Next.js inference.`,
76+
);
77+
}
78+
} catch (error) {
79+
console.warn(
80+
`[Chat Fallback Proxy] ❌ Java Backend unavailable or timed out, fallback to local Next.js inference. Error:`,
81+
error,
82+
);
83+
}
84+
// ====== 代理失败,继续往下走,启用备选方案(本地直连 AI)======
85+
3086
try {
87+
// 先把 body 消费掉,再并行验证用户身份
3188
const {
3289
messages,
3390
system,
@@ -37,6 +94,9 @@ export async function POST(req: Request) {
3794
chatId,
3895
}: ChatRequest = await req.json();
3996

97+
// 并行解析用户身份(不阻塞主流程,失败静默降级为匿名)
98+
const userIdPromise = resolveUserId(req);
99+
40100
// 对指定Provider验证key是否存在
41101
if (requiresApiKey(provider) && (!apiKey || apiKey.trim() === "")) {
42102
return Response.json(
@@ -90,22 +150,30 @@ export async function POST(req: Request) {
90150
// 根据Provider获取 AI 模型实例
91151
const model = getModel(provider, apiKey);
92152

93-
// 确保有 chatId (如果前端没传,就生成一个临时的,虽然这会导致每次请求都是新会话)
94-
// 理想情况是前端应该维护 chatId
95153
const effectiveChatId = chatId || crypto.randomUUID();
96154

97155
// 生成流式响应
98156
const result = streamText({
99157
model: model,
100158
system: systemMessage,
101-
messages: convertToModelMessages(messages || []),
159+
messages: await convertToModelMessages(messages || []),
102160
onFinish: async ({ text }) => {
103161
try {
104-
// 1. 保存/更新会话
162+
// 等待用户身份解析(与流式传输并行运行,此时大概率已完成)
163+
const userId = await userIdPromise;
164+
165+
// 1. 保存/更新会话,绑定用户 ID
166+
// update 也写入 userId:覆盖此前匿名创建的记录(用户登录后继续同一 chatId)
105167
await prisma.chat.upsert({
106168
where: { id: effectiveChatId },
107-
update: { updatedAt: new Date() },
108-
create: { id: effectiveChatId },
169+
update: {
170+
updatedAt: new Date(),
171+
...(userId != null && { userId }),
172+
},
173+
create: {
174+
id: effectiveChatId,
175+
...(userId != null && { userId }),
176+
},
109177
});
110178

111179
// 2. 保存用户消息 (取最后一条)

app/api/upload/route.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { auth } from "@/auth";
21
import { NextRequest, NextResponse } from "next/server";
2+
import type { UserView } from "@/lib/use-auth";
33
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
44
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
55
import { sanitizeDocumentSlug, sanitizeResourceKey } from "@/lib/sanitizer";
@@ -36,12 +36,22 @@ interface UploadRequest {
3636
*/
3737
export async function POST(request: NextRequest) {
3838
try {
39-
// 验证用户身份
40-
const session = await auth();
39+
// 从请求头读取 x-satoken(客户端侧统一约定),转发后端时改为 satoken
40+
const token = request.headers.get("x-satoken");
41+
if (!token) {
42+
return NextResponse.json({ error: "未授权访问" }, { status: 401 });
43+
}
4144

42-
if (!session?.user?.id) {
45+
// 调用后端 /auth/me 验证 token(服务端直连后端,走 BACKEND_URL 环境变量)
46+
const backendUrl = process.env.BACKEND_URL ?? "http://localhost:8080";
47+
const meRes = await fetch(`${backendUrl}/auth/me`, {
48+
headers: { satoken: token },
49+
});
50+
if (!meRes.ok) {
4351
return NextResponse.json({ error: "未授权访问" }, { status: 401 });
4452
}
53+
const meBody = (await meRes.json()) as { data: UserView };
54+
const currentUser = meBody.data;
4555

4656
// 验证环境变量
4757
if (
@@ -81,7 +91,7 @@ export async function POST(request: NextRequest) {
8191
// 生成唯一的对象键
8292
// 格式:users/{userId}/{article-slug}/{timestamp}-{filename}
8393
const timestamp = Date.now();
84-
const userId = session.user.id;
94+
const userId = String(currentUser.id);
8595
const sanitizedSlug = sanitizeDocumentSlug(articleSlug);
8696
const sanitizedFilename = sanitizeResourceKey(filename);
8797
const key = `users/${userId}/${sanitizedSlug}/${timestamp}-${sanitizedFilename}`;

app/components/AuthNav.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
"use client";
22

3-
import { useSession } from "next-auth/react";
3+
import { useAuth } from "@/lib/use-auth";
44
import { SignInButton } from "@/app/components/SignInButton";
55
import { UserMenu } from "@/app/components/UserMenu";
66

77
export function AuthNav() {
8-
const { data: session, status } = useSession();
8+
const { user, status, logout } = useAuth();
99

1010
if (status === "loading") {
1111
return <div className="size-9 rounded-full bg-muted animate-pulse" />;
1212
}
1313

14-
const user = session?.user;
15-
const provider =
16-
session && "provider" in session
17-
? (session.provider as string | undefined)
18-
: undefined;
19-
20-
return user ? <UserMenu user={user} provider={provider} /> : <SignInButton />;
14+
return user ? (
15+
<UserMenu
16+
user={{
17+
name: user.displayName,
18+
email: user.email ?? null,
19+
image: user.avatarUrl ?? null,
20+
}}
21+
provider="github"
22+
logout={logout}
23+
/>
24+
) : (
25+
<SignInButton />
26+
);
2127
}

0 commit comments

Comments
 (0)