Skip to content

Commit 0af209c

Browse files
committed
支持使用公开部署的服务上传图片
1 parent 36dcc78 commit 0af209c

5 files changed

Lines changed: 1309 additions & 49 deletions

File tree

app/pages/Kling.tsx

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Kling, KlingApiTypes, KlingTask } from "@/app/providers/kling";
1818
import { renderCode, RenderSubmitter } from "../render";
1919
import { beforeUpload, safeJsonStringify } from "../utils";
2020
import { ascetic } from "react-syntax-highlighter/dist/esm/styles/hljs";
21+
import { uploadToGetFileUrl } from "../utils/upload-to-server";
2122

2223
const MODE_OPTIONS = [
2324
{ label: "Std-标准模式(高性能)", value: "std" },
@@ -48,6 +49,8 @@ const KlingPage = () => {
4849
const [klingImage2VideoForm] = ProForm.useForm();
4950
const [klingQueryTaskForm] = ProForm.useForm();
5051

52+
const [uploadType, setUploadType] = useState<"base64" | "url">("base64");
53+
5154
const [taskData, setTaskData] = useState<KlingTask[]>([]);
5255
const [errorData, setErrorData] = useState<any>(null);
5356

@@ -334,7 +337,6 @@ const KlingPage = () => {
334337
updateTask: (data: KlingTask) => void;
335338
updateError: (error: any) => void;
336339
}) => {
337-
const [uploadType, setUploadType] = useState<"base64" | "url">("base64");
338340
const [abortController, setAbortController] = useState<AbortController | null>(null);
339341
const [submitting, setSubmitting] = useState(false);
340342

@@ -390,13 +392,11 @@ const KlingPage = () => {
390392
label="Upload Type"
391393
options={[
392394
{ label: "Base64", value: "base64" },
393-
// TODO: 支持url上传,目前接口维护,后续开放
394-
{ label: "URL", value: "url", disabled: true },
395+
{ label: "URL", value: "url" },
395396
]}
396397
fieldProps={{
397398
value: uploadType,
398399
onChange: (e) => {
399-
// 当上传类型改变时,清空图片和图片尾帧当上传类型改变时,清空图片和图片尾帧
400400
props.form.resetFields(["image", "image_tail"]);
401401
setUploadType(e.target.value);
402402
},
@@ -417,7 +417,6 @@ const KlingPage = () => {
417417
<p>图片文件大小不能超过10MB,图片分辨率不小于300*300px</p>
418418
</>
419419
}
420-
action={uploadType === "url" ? appConfig.getUploadConfig().action : undefined}
421420
fieldProps={{
422421
listType: "picture-card",
423422
beforeUpload: async (file) =>
@@ -431,23 +430,30 @@ const KlingPage = () => {
431430
(msg) => message.error(msg),
432431
),
433432
...(uploadType === "url" && {
434-
headers: {
435-
Authorization: appConfig.getUploadConfig().auth,
433+
customRequest: async (options) => {
434+
try {
435+
const fileUrl = await uploadToGetFileUrl(options.file as File);
436+
if (fileUrl) {
437+
options.onSuccess?.(fileUrl, options.file);
438+
} else {
439+
options.onError?.(new Error("上传失败"), options.file);
440+
}
441+
} catch (error) {
442+
console.error("Upload error:", error);
443+
options.onError?.(error as Error, options.file);
444+
}
436445
},
437446
onChange: (info) => {
438-
const getValueByPosition = (obj: any, position: readonly any[]) => {
439-
return position.reduce((acc, key) => acc && acc[key], obj);
440-
};
441-
442447
if (info.file.status === "done") {
443-
try {
444-
const response = info.file.response;
445-
if (response) {
446-
info.file.url = getValueByPosition(response, appConfig.getUploadConfig().position);
447-
}
448-
} catch (e) {
449-
console.error(e);
448+
const fileUrl = info.file.response;
449+
if (fileUrl && typeof fileUrl === "string") {
450+
info.file.url = fileUrl;
451+
} else {
452+
info.file.status = "error";
453+
message.error("上传失败");
450454
}
455+
} else if (info.file.status === "error") {
456+
message.error("上传失败");
451457
}
452458
},
453459
}),
@@ -476,7 +482,6 @@ const KlingPage = () => {
476482
<p>图片文件大小不能超过10MB,图片分辨率不小于300*300px</p>
477483
</>
478484
}
479-
action={uploadType === "url" ? appConfig.getUploadConfig().action : undefined}
480485
fieldProps={{
481486
listType: "picture-card",
482487
beforeUpload: async (file) =>
@@ -490,23 +495,30 @@ const KlingPage = () => {
490495
(msg) => message.error(msg),
491496
),
492497
...(uploadType === "url" && {
493-
headers: {
494-
Authorization: appConfig.getUploadConfig().auth,
498+
customRequest: async (options) => {
499+
try {
500+
const fileUrl = await uploadToGetFileUrl(options.file as File);
501+
if (fileUrl) {
502+
options.onSuccess?.(fileUrl, options.file);
503+
} else {
504+
options.onError?.(new Error("上传失败"), options.file);
505+
}
506+
} catch (error) {
507+
console.error("Upload error:", error);
508+
options.onError?.(error as Error, options.file);
509+
}
495510
},
496511
onChange: (info) => {
497-
const getValueByPosition = (obj: any, position: readonly any[]) => {
498-
return position.reduce((acc, key) => acc && acc[key], obj);
499-
};
500-
501512
if (info.file.status === "done") {
502-
try {
503-
const response = info.file.response;
504-
if (response) {
505-
info.file.url = getValueByPosition(response, appConfig.getUploadConfig().position);
506-
}
507-
} catch (e) {
508-
console.error(e);
513+
const fileUrl = info.file.response;
514+
if (fileUrl && typeof fileUrl === "string") {
515+
info.file.url = fileUrl;
516+
} else {
517+
info.file.status = "error";
518+
message.error("上传失败");
509519
}
520+
} else if (info.file.status === "error") {
521+
message.error("上传失败");
510522
}
511523
},
512524
}),

app/store/config.ts

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ export enum Theme {
55
Light = "light",
66
}
77

8-
interface APIKey {
9-
provider: Provider;
10-
apiKey: string;
11-
}
8+
type APIKeys = Array<{ apiKey: string }>;
129

1310
type AvailableUploadServerProvider = Provider.NextAPI | Provider.ProxyAPI;
1411

@@ -77,11 +74,15 @@ export const api2ProviderBaseUrl = {
7774

7875
export const UPLOAD_INFO = {
7976
[Provider.NextAPI]: {
80-
action: [[ProviderBaseUrlMap[Provider.NextAPI]], "fileSystem/upload"].join("/"),
77+
action: [[ProviderBaseUrlMap[Provider.NextAPI]], "fileSystem/upload"].join(
78+
"/",
79+
),
8180
position: ["url"],
8281
},
8382
[Provider.ProxyAPI]: {
84-
action: [[ProviderBaseUrlMap[Provider.ProxyAPI]], "fileSystem/upload"].join("/"),
83+
action: [[ProviderBaseUrlMap[Provider.ProxyAPI]], "fileSystem/upload"].join(
84+
"/",
85+
),
8586
position: ["url"],
8687
},
8788
// [Provider.GodAPI]: {
@@ -91,23 +92,16 @@ export const UPLOAD_INFO = {
9192
} as const;
9293

9394
const DEFAULT_CONFIG = {
94-
apiKeys: [] as APIKey[],
95-
uploadServerProvider: Provider.NextAPI as AvailableUploadServerProvider,
95+
apiKeys: [] as APIKeys,
9696
theme: Theme.Light as Theme,
9797
lastUpdate: Date.now(),
9898
} as const;
9999

100100
export const useAppConfig = createPersistStore(
101101
{ ...DEFAULT_CONFIG },
102102
(_set, get) => ({
103-
getFirstApiKey(provider: Provider): string {
104-
const key = get().apiKeys.find((item) => item.provider === provider)?.apiKey;
105-
if (key) {
106-
return key;
107-
} else {
108-
console.error(`No API key found for provider ${PROVIDER_NAME[provider]}`);
109-
return "";
110-
}
103+
getSelectedApiKey(): string {
104+
return get().apiKeys[0]?.apiKey || "";
111105
},
112106

113107
getUploadConfig(): UploadConfig {
@@ -118,7 +112,7 @@ export const useAppConfig = createPersistStore(
118112
return {
119113
action: UPLOAD_INFO[provider].action,
120114
position: UPLOAD_INFO[provider].position,
121-
auth: "Bearer " + this.getFirstApiKey(provider),
115+
auth: "Bearer " + this.getSelectedApiKey(provider),
122116
};
123117
},
124118
}),

app/utils/upload-to-server.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// FileCodeBox
2+
3+
const SERVER_URL = "https://uploader.shell-api.com";
4+
5+
// 上传
6+
// curl 'https://uploader.shell-api.com/share/file/' \
7+
// -H 'accept: */*' \
8+
// -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary45julFmDaGJVCTNt' \
9+
// --data-raw $'------WebKitFormBoundary45julFmDaGJVCTNt\r\nContent-Disposition: form-data; name="file"; filename="a5163c4c-ed8a-4251-bd4e-56cbf7689c49 (2).ico"\r\nContent-Type: image/vnd.microsoft.icon\r\n\r\n\r\n------WebKitFormBoundary45julFmDaGJVCTNt\r\nContent-Disposition: form-data; name="expire_value"\r\n\r\n1\r\n------WebKitFormBoundary45julFmDaGJVCTNt\r\nContent-Disposition: form-data; name="expire_style"\r\n\r\nminute\r\n------WebKitFormBoundary45julFmDaGJVCTNt--\r\n'
10+
11+
interface UploadToServerResponse {
12+
/**
13+
* 200 - 成功
14+
*/
15+
code: number;
16+
message: string;
17+
detail: {
18+
/**
19+
* xxxxx,五位数密码
20+
*/
21+
code: number;
22+
/**
23+
* 文件名
24+
*/
25+
name: string;
26+
};
27+
}
28+
29+
// 查询
30+
// http://uploader.shell-api.com/#/?code=84340
31+
32+
// curl 'http://uploader.shell-api.com/share/select/' \
33+
// --data-raw '{"code":"89577"}' \
34+
// --insecure
35+
36+
// {
37+
// "code": 200,
38+
// "message": "ok",
39+
// "detail": {
40+
// "code": "89577",
41+
// "name": "a5163c4c-ed8a-4251-bd4e-56cbf7689c49 (3).ico",
42+
// "size": 9259,
43+
// "text": "/share/download?key=1fe9d27aed68a599abd5cf0fea068a2ccc9f5c7482c509a97129e52b600d831d&code=89577"
44+
// }
45+
// }
46+
47+
// {"code":404,"message":"ok","detail":"文件不存在"}
48+
// {"detail":"请求次数过多,请稍后再试"}
49+
50+
interface GetFileUrlResponse {
51+
/**
52+
* 200 - 成功
53+
*/
54+
code: number;
55+
message: string;
56+
detail: {
57+
/**
58+
* 五位数密码
59+
*/
60+
code: number;
61+
/**
62+
* 文件名
63+
*/
64+
name: string;
65+
/**
66+
* 文件大小
67+
*/
68+
size: number;
69+
/**
70+
* 下载链接
71+
*/
72+
text: string;
73+
};
74+
}
75+
76+
async function _uploadToServer(file: File) {
77+
const formData = new FormData();
78+
formData.append("file", file);
79+
formData.append("expire_value", "10");
80+
formData.append("expire_style", "minute");
81+
82+
try {
83+
const response = await fetch(`${SERVER_URL}/share/file/`, {
84+
method: "POST",
85+
body: formData,
86+
});
87+
88+
const data = (await response.json()) as UploadToServerResponse;
89+
if (data.code !== 200) {
90+
throw new Error(data.message);
91+
}
92+
return data.detail.code;
93+
} catch (error) {
94+
console.error(error);
95+
throw error;
96+
}
97+
}
98+
99+
async function _getFileUrl(code: string): Promise<string> {
100+
try {
101+
const response = await fetch(`${SERVER_URL}/share/select/`, {
102+
method: "POST",
103+
body: JSON.stringify({ code }),
104+
headers: {
105+
"Content-Type": "application/json",
106+
},
107+
});
108+
const data = (await response.json()) as GetFileUrlResponse;
109+
if (data.code !== 200) {
110+
throw new Error(data?.message);
111+
}
112+
return `${SERVER_URL}${data.detail.text}`;
113+
} catch (error) {
114+
console.error(error);
115+
throw error;
116+
}
117+
}
118+
119+
export async function uploadToGetFileUrl(file: File): Promise<string> {
120+
try {
121+
const code = await _uploadToServer(file);
122+
return await _getFileUrl(code.toString());
123+
} catch (error) {
124+
console.error(error);
125+
throw error;
126+
}
127+
}

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
"@ant-design/icons": "^5.5.1",
2020
"@ant-design/nextjs-registry": "^1.0.0",
2121
"@ant-design/pro-components": "^2.7.19",
22+
"@aws-sdk/client-s3": "^3.658.1",
23+
"@aws-sdk/s3-request-presigner": "^3.658.1",
2224
"@fortaine/fetch-event-source": "^3.0.6",
2325
"@types/file-saver": "^2.0.7",
2426
"ai-model-hub": "^1.2.1",

0 commit comments

Comments
 (0)