Skip to content

Commit f202780

Browse files
夏一飞夏一飞
夏一飞
authored and
夏一飞
committed
feat: add new API routes and update deployment docs
1 parent 374c1c4 commit f202780

21 files changed

+3672
-150
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ yarn-error.log*
3838
# typescript
3939
*.tsbuildinfo
4040
next-env.d.ts
41+
.env*.local

README.md

+23
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,26 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next
3434
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
3535

3636
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
37+
38+
# 项目部署指南
39+
40+
## 环境变量设置
41+
42+
在 Vercel 部署时,需要设置以下环境变量:
43+
44+
- `POSTGRES_URL`: PostgreSQL 数据库连接 URL
45+
- `OPENWEBUI_DOMAIN`: OpenWebUI 域名
46+
- `JWT_TOKEN`: JWT 认证令牌
47+
48+
## 自动数据库迁移
49+
50+
数据库结构会在部署时自动创建和更新。项目使用 Prisma 进行数据库管理,在每次部署时会自动运行必要的数据库迁移。
51+
52+
## 部署步骤
53+
54+
1. Fork 本项目到你的 GitHub
55+
2. 在 Vercel 中导入项目
56+
3. 设置必要的环境变量
57+
4. 部署项目
58+
59+
数据库结构会在首次部署时自动创建。

app/api/files/route.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { list } from "@vercel/blob";
2+
import { NextResponse } from "next/server";
3+
4+
export async function GET() {
5+
try {
6+
const { blobs } = await list();
7+
return NextResponse.json({ files: blobs });
8+
} catch (error) {
9+
console.error("获取文件列表失败:", error);
10+
return NextResponse.json({ error: "获取文件列表失败" }, { status: 500 });
11+
}
12+
}

app/api/inlet/route.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { NextResponse } from "next/server";
2+
3+
interface InletRequest {
4+
message: {
5+
// 消息相关字段
6+
content: string;
7+
timestamp: string;
8+
// ... 其他消息字段
9+
};
10+
userInfo: {
11+
// 用户相关字段
12+
userId: string;
13+
username: string;
14+
// ... 其他用户信息字段
15+
};
16+
}
17+
18+
export async function POST(request: Request) {
19+
try {
20+
const data: InletRequest = await request.json();
21+
22+
// 记录接收到的数据
23+
console.log("收到 inlet 请求:", JSON.stringify(data));
24+
25+
// 返回占位符响应
26+
return NextResponse.json({
27+
status: "success",
28+
message: "inlet 请求已处理",
29+
timestamp: new Date().toISOString(),
30+
placeholder: {
31+
messageReceived: true,
32+
processingTime: "0.1s",
33+
requestId: Math.random().toString(36).substring(7),
34+
},
35+
});
36+
} catch (error) {
37+
console.error("处理 inlet 请求失败:", error);
38+
return NextResponse.json({ error: "处理请求失败" }, { status: 500 });
39+
}
40+
}

app/api/messages/route.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NextResponse } from "next/server";
2+
import { db, messages } from "@/lib/db";
3+
4+
// GET 请求处理
5+
export async function GET() {
6+
try {
7+
const allMessages = await db.select().from(messages);
8+
return NextResponse.json({ messages: allMessages });
9+
} catch (err) {
10+
console.error("获取消息失败:", err);
11+
return NextResponse.json({ error: "获取消息失败" }, { status: 500 });
12+
}
13+
}
14+
15+
// POST 请求处理
16+
export async function POST(request: Request) {
17+
try {
18+
const { content, fileUrl } = await request.json();
19+
const newMessage = await db
20+
.insert(messages)
21+
.values({
22+
content,
23+
fileUrl: fileUrl || null,
24+
})
25+
.returning();
26+
return NextResponse.json({ message: newMessage[0] });
27+
} catch (err) {
28+
console.error("创建消息失败:", err);
29+
return NextResponse.json({ error: "创建消息失败" }, { status: 500 });
30+
}
31+
}

app/api/models/route.ts

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { NextResponse } from "next/server";
2+
import { prisma } from "@/lib/prisma";
3+
4+
interface ModelInfo {
5+
id: string;
6+
name: string;
7+
meta: {
8+
profile_image_url: string;
9+
};
10+
}
11+
12+
interface ModelResponse {
13+
data: {
14+
info: ModelInfo;
15+
}[];
16+
}
17+
18+
function normalizeUrl(domain: string): string {
19+
if (!domain) return "";
20+
21+
// 移除首尾空格
22+
let url = domain.trim();
23+
24+
// 如果没有协议前缀,添加 https://
25+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
26+
url = "https://" + url;
27+
}
28+
29+
// 移除末尾的斜杠
30+
url = url.replace(/\/+$/, "");
31+
32+
return url;
33+
}
34+
35+
export async function GET() {
36+
try {
37+
const domain = process.env.OPENWEBUI_DOMAIN;
38+
if (!domain) {
39+
throw new Error("OPENWEBUI_DOMAIN 环境变量未设置");
40+
}
41+
42+
const normalizedDomain = normalizeUrl(domain);
43+
if (!normalizedDomain) {
44+
throw new Error("无效的域名格式");
45+
}
46+
47+
const apiUrl = `${normalizedDomain}/api/models`;
48+
console.log("Requesting URL:", apiUrl); // 调试日志
49+
50+
const response = await fetch(apiUrl, {
51+
headers: {
52+
Authorization: `Bearer ${process.env.JWT_TOKEN || ""}`,
53+
},
54+
// 添加超时设置
55+
signal: AbortSignal.timeout(10000), // 10 秒超时
56+
});
57+
58+
if (!response.ok) {
59+
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
60+
}
61+
62+
const data = (await response.json()) as ModelResponse;
63+
if (!data || !Array.isArray(data.data)) {
64+
throw new Error("返回数据格式错误");
65+
}
66+
67+
// 获取所有模型的价格信息
68+
const modelPrices = await prisma.modelPrice.findMany();
69+
const priceMap = new Map(modelPrices.map((mp) => [mp.id, mp]));
70+
71+
// 合并API返回的模型信息和价格信息
72+
const models = await Promise.all(
73+
data.data.map(async (item) => {
74+
if (!item.info || !item.info.id) {
75+
console.warn("Invalid model data:", item);
76+
return null;
77+
}
78+
79+
let priceInfo = priceMap.get(item.info.id);
80+
81+
// 如果是新模型,创建默认价格记录
82+
if (!priceInfo) {
83+
try {
84+
priceInfo = await prisma.modelPrice.create({
85+
data: {
86+
id: item.info.id,
87+
name: item.info.name || "Unknown Model",
88+
inputPrice: 60,
89+
outputPrice: 60,
90+
},
91+
});
92+
} catch (err) {
93+
console.error("Error creating price record:", err);
94+
return null;
95+
}
96+
}
97+
98+
return {
99+
id: item.info.id,
100+
name: item.info.name || "Unknown Model",
101+
imageUrl: item.info.meta?.profile_image_url || "",
102+
inputPrice: priceInfo.inputPrice,
103+
outputPrice: priceInfo.outputPrice,
104+
};
105+
})
106+
);
107+
108+
// 过滤掉无效的模型数据
109+
const validModels = models.filter(
110+
(model): model is NonNullable<typeof model> => model !== null
111+
);
112+
113+
return NextResponse.json(validModels);
114+
} catch (error) {
115+
console.error(
116+
"Error fetching models:",
117+
error instanceof Error ? error.message : "Unknown error"
118+
);
119+
return NextResponse.json(
120+
{ error: error instanceof Error ? error.message : "获取模型失败" },
121+
{ status: 500 }
122+
);
123+
}
124+
}
125+
126+
// 添加更新价格的端点
127+
export async function PUT(request: Request) {
128+
try {
129+
const body = await request.json();
130+
const { id, inputPrice, outputPrice } = body;
131+
132+
const updatedPrice = await prisma.modelPrice.update({
133+
where: { id },
134+
data: {
135+
inputPrice: parseFloat(inputPrice),
136+
outputPrice: parseFloat(outputPrice),
137+
},
138+
});
139+
140+
return NextResponse.json(updatedPrice);
141+
} catch (error) {
142+
console.error("Error updating model price:", error);
143+
return NextResponse.json(
144+
{ error: "Failed to update model price" },
145+
{ status: 500 }
146+
);
147+
}
148+
}

app/api/outlet/route.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { NextResponse } from "next/server";
2+
3+
interface OutletRequest {
4+
message: {
5+
// 消息相关字段
6+
content: string;
7+
timestamp: string;
8+
// ... 其他消息字段
9+
};
10+
userInfo: {
11+
// 用户相关字段
12+
userId: string;
13+
username: string;
14+
// ... 其他用户信息字段
15+
};
16+
}
17+
18+
export async function POST(request: Request) {
19+
try {
20+
const data: OutletRequest = await request.json();
21+
22+
// 记录接收到的数据
23+
console.log("收到 outlet 请求:", JSON.stringify(data));
24+
25+
// 返回占位符响应
26+
return NextResponse.json({
27+
status: "success",
28+
message: "outlet 请求已处理",
29+
timestamp: new Date().toISOString(),
30+
placeholder: {
31+
messageProcessed: true,
32+
responseTime: "0.2s",
33+
sessionId: Math.random().toString(36).substring(7),
34+
},
35+
});
36+
} catch (error) {
37+
console.error("处理 outlet 请求失败:", error);
38+
return NextResponse.json({ error: "处理请求失败" }, { status: 500 });
39+
}
40+
}

app/api/upload/route.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { put } from "@vercel/blob";
2+
import { NextResponse } from "next/server";
3+
4+
export async function POST(request: Request): Promise<NextResponse> {
5+
try {
6+
const { searchParams } = new URL(request.url);
7+
const filename = searchParams.get("filename");
8+
9+
if (!filename) {
10+
return NextResponse.json({ error: "缺少文件名" }, { status: 400 });
11+
}
12+
13+
if (!request.body) {
14+
return NextResponse.json({ error: "缺少文件内容" }, { status: 400 });
15+
}
16+
17+
const blob = await put(filename, request.body, {
18+
access: "public",
19+
});
20+
21+
return NextResponse.json(blob);
22+
} catch (error) {
23+
console.error("文件上传失败:", error);
24+
return NextResponse.json({ error: "文件上传失败" }, { status: 500 });
25+
}
26+
}

app/components/Navigation.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Link from "next/link";
2+
3+
<Link href="/models" className="...">
4+
模型配置
5+
</Link>;

0 commit comments

Comments
 (0)