feat: add qiniu kodo storage configuration
This commit is contained in:
parent
d2e282030b
commit
4941f09dfa
@ -939,7 +939,7 @@ type AiModelHealthEvent = {
|
|||||||
type StorageConfig = {
|
type StorageConfig = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: "s3" | "local" | "aliyun-oss";
|
type: "s3" | "local" | "aliyun-oss" | "qiniu-kodo";
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
bucket?: string;
|
bucket?: string;
|
||||||
@ -3785,7 +3785,7 @@ export function AdminSettingsClient() {
|
|||||||
const [storageForm, setStorageForm] = useState({
|
const [storageForm, setStorageForm] = useState({
|
||||||
id: "",
|
id: "",
|
||||||
name: "",
|
name: "",
|
||||||
type: "s3" as "s3" | "local" | "aliyun-oss",
|
type: "s3" as "s3" | "local" | "aliyun-oss" | "qiniu-kodo",
|
||||||
endpoint: "",
|
endpoint: "",
|
||||||
region: "",
|
region: "",
|
||||||
bucket: "",
|
bucket: "",
|
||||||
@ -7252,6 +7252,10 @@ export function AdminSettingsClient() {
|
|||||||
body.secretAccessKey = String(body.secretAccessKey || "").trim();
|
body.secretAccessKey = String(body.secretAccessKey || "").trim();
|
||||||
body.publicUrl = normalizeUrl(body.publicUrl);
|
body.publicUrl = normalizeUrl(body.publicUrl);
|
||||||
|
|
||||||
|
if (body.type === "qiniu-kodo" && !body.publicUrl) {
|
||||||
|
throw new Error("七牛云 Kodo 需要配置公开访问域名");
|
||||||
|
}
|
||||||
|
|
||||||
// 兼容处理:如果是阿里云 OSS,转换为 s3 类型提交
|
// 兼容处理:如果是阿里云 OSS,转换为 s3 类型提交
|
||||||
if (body.type === "aliyun-oss") {
|
if (body.type === "aliyun-oss") {
|
||||||
body.type = "s3";
|
body.type = "s3";
|
||||||
@ -7275,7 +7279,7 @@ export function AdminSettingsClient() {
|
|||||||
setShowStorageDialog(false);
|
setShowStorageDialog(false);
|
||||||
resetStorageForm();
|
resetStorageForm();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("保存存储配置失败");
|
toast.error(error instanceof Error ? error.message : "保存存储配置失败");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -16498,7 +16502,9 @@ export function AdminSettingsClient() {
|
|||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>存储配置</CardTitle>
|
<CardTitle>存储配置</CardTitle>
|
||||||
<CardDescription>配置 S3 兼容对象存储</CardDescription>
|
<CardDescription>
|
||||||
|
配置 S3 兼容对象存储、阿里云 OSS 或七牛云 Kodo
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Dialog
|
<Dialog
|
||||||
onOpenChange={setShowStorageDialog}
|
onOpenChange={setShowStorageDialog}
|
||||||
@ -16547,12 +16553,14 @@ export function AdminSettingsClient() {
|
|||||||
<SelectContent className="!bg-white !dark:bg-zinc-950">
|
<SelectContent className="!bg-white !dark:bg-zinc-950">
|
||||||
<SelectItem value="s3">S3 兼容</SelectItem>
|
<SelectItem value="s3">S3 兼容</SelectItem>
|
||||||
<SelectItem value="aliyun-oss">阿里云 OSS</SelectItem>
|
<SelectItem value="aliyun-oss">阿里云 OSS</SelectItem>
|
||||||
|
<SelectItem value="qiniu-kodo">七牛云 Kodo</SelectItem>
|
||||||
<SelectItem value="local">本地存储</SelectItem>
|
<SelectItem value="local">本地存储</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
{(storageForm.type === "s3" ||
|
{(storageForm.type === "s3" ||
|
||||||
storageForm.type === "aliyun-oss") && (
|
storageForm.type === "aliyun-oss" ||
|
||||||
|
storageForm.type === "qiniu-kodo") && (
|
||||||
<>
|
<>
|
||||||
{storageForm.type === "aliyun-oss" && (
|
{storageForm.type === "aliyun-oss" && (
|
||||||
<div className="mb-4 rounded-md border border-blue-100 bg-blue-50 p-3 text-blue-700 text-xs">
|
<div className="mb-4 rounded-md border border-blue-100 bg-blue-50 p-3 text-blue-700 text-xs">
|
||||||
@ -16566,8 +16574,28 @@ export function AdminSettingsClient() {
|
|||||||
<p>• 请确保 AccessKey 拥有对应 Bucket 的读写权限</p>
|
<p>• 请确保 AccessKey 拥有对应 Bucket 的读写权限</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{storageForm.type === "qiniu-kodo" && (
|
||||||
|
<div className="mb-4 rounded-md border border-emerald-100 bg-emerald-50 p-3 text-emerald-700 text-xs">
|
||||||
|
<p className="mb-1 font-semibold">
|
||||||
|
七牛云 Kodo 配置说明:
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
• Endpoint 填写七牛 S3 兼容访问端点,例如:https://s3.cn-east-1.qiniucs.com
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
• Region 填写七牛 S3 Region ID,例如:cn-east-1
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
• Public URL 必填公开访问域名,例如:https://img.example.com
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>端点 (Endpoint)</Label>
|
<Label>
|
||||||
|
{storageForm.type === "qiniu-kodo"
|
||||||
|
? "七牛 S3 Endpoint"
|
||||||
|
: "端点 (Endpoint)"}
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setStorageForm({
|
setStorageForm({
|
||||||
@ -16591,7 +16619,11 @@ export function AdminSettingsClient() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Access Key</Label>
|
<Label>
|
||||||
|
{storageForm.type === "qiniu-kodo"
|
||||||
|
? "AccessKey"
|
||||||
|
: "Access Key"}
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setStorageForm({
|
setStorageForm({
|
||||||
@ -16604,7 +16636,11 @@ export function AdminSettingsClient() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Secret Key</Label>
|
<Label>
|
||||||
|
{storageForm.type === "qiniu-kodo"
|
||||||
|
? "SecretKey"
|
||||||
|
: "Secret Key"}
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setStorageForm({
|
setStorageForm({
|
||||||
@ -16618,7 +16654,11 @@ export function AdminSettingsClient() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Region</Label>
|
<Label>
|
||||||
|
{storageForm.type === "qiniu-kodo"
|
||||||
|
? "Region ID"
|
||||||
|
: "Region"}
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setStorageForm({
|
setStorageForm({
|
||||||
@ -16630,7 +16670,11 @@ export function AdminSettingsClient() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Public URL</Label>
|
<Label>
|
||||||
|
{storageForm.type === "qiniu-kodo"
|
||||||
|
? "公开访问域名 (Public URL)"
|
||||||
|
: "Public URL"}
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setStorageForm({
|
setStorageForm({
|
||||||
@ -16708,7 +16752,9 @@ export function AdminSettingsClient() {
|
|||||||
? (config.endpoint?.includes("aliyuncs.com")
|
? (config.endpoint?.includes("aliyuncs.com")
|
||||||
? "阿里云 OSS"
|
? "阿里云 OSS"
|
||||||
: "S3") + ` | Bucket: ${config.bucket}`
|
: "S3") + ` | Bucket: ${config.bucket}`
|
||||||
: "本地存储"}
|
: config.type === "qiniu-kodo"
|
||||||
|
? `七牛云 Kodo | Bucket: ${config.bucket}`
|
||||||
|
: "本地存储"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -16718,10 +16764,12 @@ export function AdminSettingsClient() {
|
|||||||
id: config.id,
|
id: config.id,
|
||||||
name: config.name,
|
name: config.name,
|
||||||
type:
|
type:
|
||||||
config.type === "s3" &&
|
config.type === "qiniu-kodo"
|
||||||
config.endpoint?.includes("aliyuncs.com")
|
? "qiniu-kodo"
|
||||||
? "aliyun-oss"
|
: config.type === "s3" &&
|
||||||
: (config.type as any),
|
config.endpoint?.includes("aliyuncs.com")
|
||||||
|
? "aliyun-oss"
|
||||||
|
: (config.type as any),
|
||||||
endpoint: config.endpoint || "",
|
endpoint: config.endpoint || "",
|
||||||
region: config.region || "",
|
region: config.region || "",
|
||||||
bucket: config.bucket || "",
|
bucket: config.bucket || "",
|
||||||
@ -16757,8 +16805,8 @@ export function AdminSettingsClient() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>存储故障迁移</CardTitle>
|
<CardTitle>存储故障迁移</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
主存储桶上传失败时,自动切换到备用 MinIO/S3/阿里云 OSS 等 S3
|
主存储桶上传失败时,自动切换到备用 MinIO/S3/阿里云
|
||||||
兼容存储桶。
|
OSS/七牛云 Kodo 等对象存储桶。
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { ChatSDKError } from "@/lib/errors";
|
|||||||
|
|
||||||
const createStorageSchema = z.object({
|
const createStorageSchema = z.object({
|
||||||
name: z.string().trim().min(1).max(128),
|
name: z.string().trim().min(1).max(128),
|
||||||
type: z.enum(["s3", "local"]),
|
type: z.enum(["s3", "local", "qiniu-kodo"]),
|
||||||
endpoint: z.string().trim().optional(),
|
endpoint: z.string().trim().optional(),
|
||||||
region: z.string().trim().max(64).optional(),
|
region: z.string().trim().max(64).optional(),
|
||||||
bucket: z.string().trim().max(128).optional(),
|
bucket: z.string().trim().max(128).optional(),
|
||||||
@ -25,7 +25,7 @@ const createStorageSchema = z.object({
|
|||||||
const updateStorageSchema = z.object({
|
const updateStorageSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
name: z.string().trim().min(1).max(128).optional(),
|
name: z.string().trim().min(1).max(128).optional(),
|
||||||
type: z.enum(["s3", "local"]).optional(),
|
type: z.enum(["s3", "local", "qiniu-kodo"]).optional(),
|
||||||
endpoint: z.string().trim().optional(),
|
endpoint: z.string().trim().optional(),
|
||||||
region: z.string().trim().max(64).optional(),
|
region: z.string().trim().max(64).optional(),
|
||||||
bucket: z.string().trim().max(128).optional(),
|
bucket: z.string().trim().max(128).optional(),
|
||||||
|
|||||||
@ -4423,7 +4423,7 @@ export async function createStorageConfig({
|
|||||||
updatedBy,
|
updatedBy,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
type: "s3" | "local";
|
type: "s3" | "local" | "qiniu-kodo";
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
bucket?: string;
|
bucket?: string;
|
||||||
@ -4483,7 +4483,7 @@ export async function updateStorageConfig({
|
|||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
type?: "s3" | "local";
|
type?: "s3" | "local" | "qiniu-kodo";
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
bucket?: string;
|
bucket?: string;
|
||||||
|
|||||||
@ -392,8 +392,8 @@ export type AiModelHealthEvent = InferSelectModel<typeof aiModelHealthEvent>;
|
|||||||
export const storageConfig = pgTable("StorageConfig", {
|
export const storageConfig = pgTable("StorageConfig", {
|
||||||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||||||
name: varchar("name", { length: 128 }).notNull(), // 配置名称
|
name: varchar("name", { length: 128 }).notNull(), // 配置名称
|
||||||
type: varchar("type", { enum: ["s3", "local"] }).notNull().default("s3"),
|
type: varchar("type", { enum: ["s3", "local", "qiniu-kodo"] }).notNull().default("s3"),
|
||||||
endpoint: text("endpoint"), // S3 端点
|
endpoint: text("endpoint"), // S3/七牛 Kodo 兼容端点
|
||||||
region: varchar("region", { length: 64 }), // 区域
|
region: varchar("region", { length: 64 }), // 区域
|
||||||
bucket: varchar("bucket", { length: 128 }), // 存储桶名称
|
bucket: varchar("bucket", { length: 128 }), // 存储桶名称
|
||||||
accessKeyId: text("accessKeyId"), // Access Key ID
|
accessKeyId: text("accessKeyId"), // Access Key ID
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export type S3UploadConfig = {
|
|||||||
accessKeyId: string;
|
accessKeyId: string;
|
||||||
secretAccessKey: string;
|
secretAccessKey: string;
|
||||||
publicUrl?: string;
|
publicUrl?: string;
|
||||||
|
provider?: "s3" | "qiniu-kodo";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UploadedFileResult = {
|
export type UploadedFileResult = {
|
||||||
@ -116,7 +117,7 @@ export function createS3Client(config: S3UploadConfig): S3Client {
|
|||||||
accessKeyId: config.accessKeyId,
|
accessKeyId: config.accessKeyId,
|
||||||
secretAccessKey: config.secretAccessKey,
|
secretAccessKey: config.secretAccessKey,
|
||||||
},
|
},
|
||||||
forcePathStyle: !isCloudS3,
|
forcePathStyle: config.provider === "qiniu-kodo" ? true : !isCloudS3,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,6 +270,8 @@ export async function deleteS3ObjectByPath(pathname: string, config: S3UploadCon
|
|||||||
|
|
||||||
export function toDbS3UploadConfig(raw: unknown): S3UploadConfig | null {
|
export function toDbS3UploadConfig(raw: unknown): S3UploadConfig | null {
|
||||||
const value = raw as Record<string, unknown> | null | undefined;
|
const value = raw as Record<string, unknown> | null | undefined;
|
||||||
|
const provider =
|
||||||
|
value?.type === "qiniu-kodo" ? "qiniu-kodo" : value?.type === "s3" ? "s3" : null;
|
||||||
const endpoint = value?.endpoint ? normalizeEndpoint(String(value.endpoint)) : "";
|
const endpoint = value?.endpoint ? normalizeEndpoint(String(value.endpoint)) : "";
|
||||||
const bucket = value?.bucket ? String(value.bucket).trim() : "";
|
const bucket = value?.bucket ? String(value.bucket).trim() : "";
|
||||||
const accessKeyId = value?.accessKeyId ? String(value.accessKeyId).trim() : "";
|
const accessKeyId = value?.accessKeyId ? String(value.accessKeyId).trim() : "";
|
||||||
@ -278,7 +281,7 @@ export function toDbS3UploadConfig(raw: unknown): S3UploadConfig | null {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
!value ||
|
!value ||
|
||||||
value.type !== "s3" ||
|
!provider ||
|
||||||
!value.isEnabled ||
|
!value.isEnabled ||
|
||||||
!endpoint ||
|
!endpoint ||
|
||||||
!bucket ||
|
!bucket ||
|
||||||
@ -295,6 +298,7 @@ export function toDbS3UploadConfig(raw: unknown): S3UploadConfig | null {
|
|||||||
accessKeyId,
|
accessKeyId,
|
||||||
secretAccessKey,
|
secretAccessKey,
|
||||||
publicUrl: normalizePublicUrl(value.publicUrl ? String(value.publicUrl) : undefined),
|
publicUrl: normalizePublicUrl(value.publicUrl ? String(value.publicUrl) : undefined),
|
||||||
|
provider,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -622,7 +626,7 @@ export async function getStorageType(): Promise<
|
|||||||
const storageConfig = await getDefaultStorageConfig();
|
const storageConfig = await getDefaultStorageConfig();
|
||||||
if (
|
if (
|
||||||
storageConfig &&
|
storageConfig &&
|
||||||
storageConfig.type === "s3" &&
|
(storageConfig.type === "s3" || storageConfig.type === "qiniu-kodo") &&
|
||||||
storageConfig.endpoint &&
|
storageConfig.endpoint &&
|
||||||
storageConfig.bucket
|
storageConfig.bucket
|
||||||
) {
|
) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user