Skip to content

Commit 99dfaed

Browse files
committed
custom api
1 parent 5cc21f4 commit 99dfaed

File tree

8 files changed

+331
-0
lines changed

8 files changed

+331
-0
lines changed

modules/dataset/router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import { sourceRegistry } from './source/registry';
44
import { getDatasetSourceAvatarUrl, getDatasetSourceOutlineAvatarUrl } from './avatars';
55

66
// 注册数据源
7+
import customApiSource from './sources/custom-api';
78
import feishuSource from './sources/feishu';
89
import yuqueSource from './sources/yuque';
910

11+
sourceRegistry.register(customApiSource);
1012
sourceRegistry.register(feishuSource);
1113
sourceRegistry.register(yuqueSource);
1214

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Plugin Dataset Source ID 常量
3+
*/
4+
export const PluginDatasetSourceIds = {
5+
customApi: 'custom-api',
6+
feishu: 'feishu',
7+
yuque: 'yuque'
8+
} as const;
9+
10+
export type PluginDatasetSourceId =
11+
(typeof PluginDatasetSourceIds)[keyof typeof PluginDatasetSourceIds];

modules/dataset/source/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { sourceRegistry, defineSource } from './registry';
22
export type { DatasetSourceCallbacks, DatasetSourceDefinition } from './registry';
3+
export { PluginDatasetSourceIds, type PluginDatasetSourceId } from './constants';
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { z } from 'zod';
2+
import type { DatasetSourceConfig } from '../../type/source';
3+
import { getDatasetSourceAvatarUrl, getDatasetSourceOutlineAvatarUrl } from '../../avatars';
4+
5+
const SOURCE_ID = 'custom-api';
6+
7+
// Config Schema
8+
export const CustomApiConfigSchema = z.object({
9+
baseUrl: z.string().min(1, 'baseUrl is required'),
10+
authorization: z.string().optional(),
11+
basePath: z.string().optional()
12+
});
13+
14+
export type CustomApiConfig = z.infer<typeof CustomApiConfigSchema>;
15+
16+
export const customApiConfig: DatasetSourceConfig = {
17+
sourceId: SOURCE_ID,
18+
name: {
19+
en: 'Custom API',
20+
'zh-CN': 'API 文件库',
21+
'zh-Hant': 'API 檔案庫'
22+
},
23+
description: {
24+
en: 'Build knowledge base using external file library through custom API',
25+
'zh-CN': '可以通过 API,使用外部文件库构建知识库',
26+
'zh-Hant': '可以透過 API,使用外部檔案庫建構知識庫'
27+
},
28+
icon: getDatasetSourceAvatarUrl(SOURCE_ID),
29+
iconOutline: getDatasetSourceOutlineAvatarUrl(SOURCE_ID),
30+
version: '1.0.0',
31+
courseUrl: '/docs/introduction/guide/knowledge_base/api_dataset/',
32+
33+
formFields: [
34+
{
35+
key: 'baseUrl',
36+
label: {
37+
en: 'API URL',
38+
'zh-CN': '接口地址',
39+
'zh-Hant': '介面地址'
40+
},
41+
type: 'input',
42+
required: true,
43+
placeholder: {
44+
en: 'Enter API base URL',
45+
'zh-CN': '请输入接口地址',
46+
'zh-Hant': '請輸入介面地址'
47+
}
48+
},
49+
{
50+
key: 'authorization',
51+
label: {
52+
en: 'Authorization',
53+
'zh-CN': '鉴权参数',
54+
'zh-Hant': '鑑權參數'
55+
},
56+
type: 'password',
57+
required: false,
58+
placeholder: {
59+
en: 'Request headers, will automatically append Bearer',
60+
'zh-CN': '请求头参数,会自动补充 Bearer',
61+
'zh-Hant': '請求頭參數,會自動補充 Bearer'
62+
}
63+
},
64+
{
65+
key: 'basePath',
66+
label: {
67+
en: 'Base Path',
68+
'zh-CN': '起始目录',
69+
'zh-Hant': '起始目錄'
70+
},
71+
type: 'tree-select',
72+
required: false,
73+
description: {
74+
en: 'Optional: Select specific folder as starting point',
75+
'zh-CN': '可选:选择特定文件夹作为起始目录',
76+
'zh-Hant': '可選:選擇特定資料夾作為起始目錄'
77+
}
78+
}
79+
]
80+
};
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { defineSource, type DatasetSourceCallbacks } from '../../source/registry';
2+
import { customApiConfig, CustomApiConfigSchema, type CustomApiConfig } from './config';
3+
import type { FileItem, FileContentResponse } from '../../type/source';
4+
5+
type ResponseDataType = {
6+
success: boolean;
7+
message: string;
8+
data: any;
9+
};
10+
11+
type APIFileListResponse = {
12+
id: string;
13+
parentId: string | null;
14+
name: string;
15+
type: 'file' | 'folder';
16+
updateTime: string;
17+
createTime: string;
18+
hasChild?: boolean;
19+
};
20+
21+
type APIFileContentResponse = {
22+
title?: string;
23+
content?: string;
24+
previewUrl?: string;
25+
};
26+
27+
type APIFileReadResponse = {
28+
url: string;
29+
};
30+
31+
type APIFileDetailResponse = {
32+
id: string;
33+
name: string;
34+
parentId: string | null;
35+
type: 'file' | 'folder';
36+
updateTime: string;
37+
createTime: string;
38+
};
39+
40+
function parseConfig(config: Record<string, any>): CustomApiConfig {
41+
return CustomApiConfigSchema.parse(config);
42+
}
43+
44+
/**
45+
* 响应数据检查
46+
*/
47+
function checkResponse(data: ResponseDataType): any {
48+
if (data === undefined) {
49+
throw new Error('服务器异常:响应为空');
50+
}
51+
if (!data.success) {
52+
throw new Error(data.message || '请求失败');
53+
}
54+
return data.data;
55+
}
56+
57+
/**
58+
* 发送请求到用户的自定义 API
59+
*/
60+
async function apiRequest<T>(
61+
baseUrl: string,
62+
authorization: string | undefined,
63+
path: string,
64+
method: 'GET' | 'POST',
65+
data?: Record<string, any>
66+
): Promise<T> {
67+
// 清理 undefined 值
68+
if (data) {
69+
for (const key in data) {
70+
if (data[key] === undefined) {
71+
delete data[key];
72+
}
73+
}
74+
}
75+
76+
const url = new URL(path, baseUrl);
77+
const isGetMethod = method === 'GET';
78+
79+
// GET 请求将参数添加到 URL
80+
if (isGetMethod && data) {
81+
Object.entries(data).forEach(([key, value]) => {
82+
if (value !== undefined && value !== null) {
83+
url.searchParams.append(key, String(value));
84+
}
85+
});
86+
}
87+
88+
const headers: Record<string, string> = {
89+
'Content-Type': 'application/json'
90+
};
91+
92+
if (authorization) {
93+
headers['Authorization'] = `Bearer ${authorization}`;
94+
}
95+
96+
const response = await fetch(url.toString(), {
97+
method,
98+
headers,
99+
body: isGetMethod ? undefined : JSON.stringify(data)
100+
});
101+
102+
if (!response.ok) {
103+
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
104+
}
105+
106+
const jsonData = await response.json();
107+
return checkResponse(jsonData);
108+
}
109+
110+
const callbacks: DatasetSourceCallbacks = {
111+
async listFiles({ config, parentId }): Promise<FileItem[]> {
112+
const { baseUrl, authorization, basePath } = parseConfig(config);
113+
114+
const files = await apiRequest<APIFileListResponse[]>(
115+
baseUrl,
116+
authorization,
117+
'/v1/file/list',
118+
'POST',
119+
{
120+
parentId: parentId || basePath
121+
}
122+
);
123+
124+
if (!Array.isArray(files)) {
125+
throw new Error('Invalid file list format');
126+
}
127+
128+
return files.map((file) => ({
129+
id: file.id,
130+
rawId: file.id,
131+
parentId: file.parentId,
132+
name: file.name,
133+
type: file.type,
134+
hasChild: file.hasChild ?? file.type === 'folder',
135+
updateTime: file.updateTime,
136+
createTime: file.createTime
137+
}));
138+
},
139+
140+
async getFileContent({ config, fileId }): Promise<FileContentResponse> {
141+
const { baseUrl, authorization } = parseConfig(config);
142+
143+
const data = await apiRequest<APIFileContentResponse>(
144+
baseUrl,
145+
authorization,
146+
'/v1/file/content',
147+
'GET',
148+
{ id: fileId }
149+
);
150+
151+
const { title, content, previewUrl } = data;
152+
153+
// 如果有直接内容,返回
154+
if (content) {
155+
return {
156+
title,
157+
rawText: content
158+
};
159+
}
160+
161+
// 如果有预览 URL,返回给 FastGPT 去解析
162+
if (previewUrl) {
163+
return {
164+
title,
165+
previewUrl
166+
};
167+
}
168+
169+
throw new Error('Invalid content type: content or previewUrl is required');
170+
},
171+
172+
async getFilePreviewUrl({ config, fileId }): Promise<string> {
173+
const { baseUrl, authorization } = parseConfig(config);
174+
175+
const data = await apiRequest<APIFileReadResponse>(
176+
baseUrl,
177+
authorization,
178+
'/v1/file/read',
179+
'GET',
180+
{ id: fileId }
181+
);
182+
183+
if (!data.url || typeof data.url !== 'string') {
184+
throw new Error('Invalid response url');
185+
}
186+
187+
return data.url;
188+
},
189+
190+
async getFileDetail({ config, fileId }): Promise<FileItem> {
191+
const { baseUrl, authorization } = parseConfig(config);
192+
193+
const data = await apiRequest<APIFileDetailResponse>(
194+
baseUrl,
195+
authorization,
196+
'/v1/file/detail',
197+
'GET',
198+
{ id: fileId }
199+
);
200+
201+
if (!data) {
202+
throw new Error('File not found');
203+
}
204+
205+
return {
206+
id: data.id,
207+
rawId: fileId,
208+
parentId: data.parentId,
209+
name: data.name,
210+
type: data.type,
211+
hasChild: data.type === 'folder',
212+
updateTime: data.updateTime,
213+
createTime: data.createTime
214+
};
215+
}
216+
};
217+
218+
export default defineSource(customApiConfig, callbacks);
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 11 additions & 0 deletions
Loading

sdk/client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,6 @@ export type {
2626
FileItem,
2727
FileContentResponse
2828
} from '@dataset/type/source';
29+
30+
// Dataset Source Constants
31+
export { PluginDatasetSourceIds, type PluginDatasetSourceId } from '@dataset/source/constants';

0 commit comments

Comments
 (0)